feat: workflow run cancel (#489)

* feat: workflow run cancel endpoint

* feat: cancel all running button

* feat: select and cancel runs

* chore: error handling

* fix: remove ani

* fix: error toast

* chore: gen

* fix: feedback

* fix: redundant method

* Update frontend/app/src/pages/main/workflow-runs/$run/index.tsx

Co-authored-by: abelanger5 <belanger@sas.upenn.edu>

* fix: response obj

* fix: cancel by job run

* fix: lint

* chore: generate

---------

Co-authored-by: abelanger5 <belanger@sas.upenn.edu>
Co-authored-by: Alexander Belanger <alexander@hatchet.run>
This commit is contained in:
Gabe Ruttner
2024-05-14 14:47:30 -07:00
committed by GitHub
parent d1835458d8
commit a37ee57cc1
15 changed files with 772 additions and 160 deletions

View File

@@ -124,6 +124,8 @@ WorkflowRunStatus:
$ref: "./workflow_run.yaml#/WorkflowRunStatus"
WorkflowRunStatusList:
$ref: "./workflow_run.yaml#/WorkflowRunStatusList"
WorkflowRunsCancelRequest:
$ref: "./workflow_run.yaml#/WorkflowRunsCancelRequest"
JobRunStatus:
$ref: "./workflow_run.yaml#/JobRunStatus"
StepRunStatus:

View File

@@ -118,6 +118,19 @@ WorkflowRunStatusList:
items:
$ref: "#/WorkflowRunStatus"
WorkflowRunsCancelRequest:
type: object
properties:
workflowRunIds:
type: array
items:
type: string
format: uuid
minLength: 36
maxLength: 36
required:
- workflowRunIds
WorkflowRunTriggeredBy:
type: object
properties:

View File

@@ -106,6 +106,8 @@ paths:
$ref: "./paths/event/event.yaml#/keys"
/api/v1/tenants/{tenant}/workflows:
$ref: "./paths/workflow/workflow.yaml#/withTenant"
/api/v1/tenants/{tenant}/workflows/cancel:
$ref: "./paths/workflow/workflow.yaml#/cancelWorkflowRuns"
/api/v1/workflows/{workflow}:
$ref: "./paths/workflow/workflow.yaml#/withWorkflow"
/api/v1/workflows/{workflow}/versions:

View File

@@ -273,6 +273,60 @@ triggerWorkflow:
summary: Trigger workflow run
tags:
- Workflow Run
cancelWorkflowRuns:
post:
x-resources: ["tenant"]
description: Cancel a batch of workflow runs
operationId: workflow-run:cancel
parameters:
- description: The tenant id
in: path
name: tenant
required: true
schema:
type: string
format: uuid
minLength: 36
maxLength: 36
requestBody:
content:
application/json:
schema:
$ref: "../../components/schemas/_index.yaml#/WorkflowRunsCancelRequest"
description: The input to cancel the workflow runs
required: true
responses:
"200":
content:
application/json:
schema:
type: object
properties:
workflowRunIds:
type: array
items:
type: string
format: uuid
minLength: 36
maxLength: 36
description: Successfully cancelled the workflow runs
"400":
content:
application/json:
schema:
$ref: "../../components/schemas/_index.yaml#/APIErrors"
description: A malformed or bad request
"403":
content:
application/json:
schema:
$ref: "../../components/schemas/_index.yaml#/APIErrors"
description: Forbidden
summary: Cancel workflow runs
tags:
- Workflow Run
workflowRuns:
get:
x-resources: ["tenant"]

View File

@@ -0,0 +1,76 @@
package workflows
import (
"fmt"
"sync"
"github.com/labstack/echo/v4"
"github.com/google/uuid"
"github.com/hashicorp/go-multierror"
"github.com/hatchet-dev/hatchet/api/v1/server/oas/gen"
"github.com/hatchet-dev/hatchet/internal/msgqueue"
"github.com/hatchet-dev/hatchet/internal/repository/prisma/db"
"github.com/hatchet-dev/hatchet/internal/repository/prisma/sqlchelpers"
"github.com/hatchet-dev/hatchet/internal/services/shared/tasktypes"
)
func (t *WorkflowService) WorkflowRunCancel(ctx echo.Context, request gen.WorkflowRunCancelRequestObject) (gen.WorkflowRunCancelResponseObject, error) {
tenant := ctx.Get("tenant").(*db.TenantModel)
runIds := request.Body.WorkflowRunIds
var wg sync.WaitGroup
var mu sync.Mutex
var cancelledWorkflowRunIds []uuid.UUID
var returnErr error
for _, runId := range runIds {
wg.Add(1)
go func(runId uuid.UUID) {
defer wg.Done()
// Lookup step runs for the workflow run
runIdStr := runId.String()
jobRun, err := t.config.EngineRepository.JobRun().ListJobRunsForWorkflowRun(ctx.Request().Context(), tenant.ID, runIdStr)
if err != nil {
returnErr = multierror.Append(err, fmt.Errorf("failed to list job runs for workflow run %s", runIdStr))
return
}
for _, jobRun := range jobRun {
// If the step run is not in a final state, send a task to the taskqueue to cancel it
var reason = "CANCELLED_BY_USER"
// send a task to the taskqueue
jobRunId := sqlchelpers.UUIDToStr(jobRun.ID)
err = t.config.MessageQueue.AddMessage(
ctx.Request().Context(),
msgqueue.JOB_PROCESSING_QUEUE,
tasktypes.JobRunCancelledToTask(tenant.ID, jobRunId, &reason),
)
if err != nil {
returnErr = multierror.Append(err, fmt.Errorf("failed to send cancel task for job run %s", jobRunId))
continue
}
}
// Add the canceled workflow run ID to the slice
mu.Lock()
cancelledWorkflowRunIds = append(cancelledWorkflowRunIds, runId)
mu.Unlock()
}(runId)
}
wg.Wait()
if returnErr != nil {
return nil, returnErr
}
// Create a new instance of gen.WorkflowRunCancel200JSONResponse and assign canceledWorkflowRunUUIDs to its WorkflowRunIds field
response := gen.WorkflowRunCancel200JSONResponse{
WorkflowRunIds: &cancelledWorkflowRunIds,
}
// Return the response
return response, nil
}

View File

@@ -945,6 +945,11 @@ type WorkflowRunTriggeredBy struct {
ParentId string `json:"parentId"`
}
// WorkflowRunsCancelRequest defines model for WorkflowRunsCancelRequest.
type WorkflowRunsCancelRequest struct {
WorkflowRunIds []openapi_types.UUID `json:"workflowRunIds"`
}
// WorkflowRunsMetrics defines model for WorkflowRunsMetrics.
type WorkflowRunsMetrics struct {
Counts *WorkflowRunsMetricsCounts `json:"counts,omitempty"`
@@ -1192,6 +1197,9 @@ type SnsCreateJSONRequestBody = CreateSNSIntegrationRequest
// StepRunUpdateRerunJSONRequestBody defines body for StepRunUpdateRerun for application/json ContentType.
type StepRunUpdateRerunJSONRequestBody = RerunStepRunRequest
// WorkflowRunCancelJSONRequestBody defines body for WorkflowRunCancel for application/json ContentType.
type WorkflowRunCancelJSONRequestBody = WorkflowRunsCancelRequest
// TenantInviteAcceptJSONRequestBody defines body for TenantInviteAccept for application/json ContentType.
type TenantInviteAcceptJSONRequestBody = AcceptInviteRequest
@@ -1356,6 +1364,9 @@ type ServerInterface interface {
// Get workflows
// (GET /api/v1/tenants/{tenant}/workflows)
WorkflowList(ctx echo.Context, tenant openapi_types.UUID) error
// Cancel workflow runs
// (POST /api/v1/tenants/{tenant}/workflows/cancel)
WorkflowRunCancel(ctx echo.Context, tenant openapi_types.UUID) error
// Get workflow runs
// (GET /api/v1/tenants/{tenant}/workflows/runs)
WorkflowRunList(ctx echo.Context, tenant openapi_types.UUID, params WorkflowRunListParams) error
@@ -2522,6 +2533,26 @@ func (w *ServerInterfaceWrapper) WorkflowList(ctx echo.Context) error {
return err
}
// WorkflowRunCancel converts echo context to params.
func (w *ServerInterfaceWrapper) WorkflowRunCancel(ctx echo.Context) error {
var err error
// ------------- Path parameter "tenant" -------------
var tenant openapi_types.UUID
err = runtime.BindStyledParameterWithLocation("simple", false, "tenant", runtime.ParamLocationPath, ctx.Param("tenant"), &tenant)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter tenant: %s", err))
}
ctx.Set(BearerAuthScopes, []string{})
ctx.Set(CookieAuthScopes, []string{})
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.WorkflowRunCancel(ctx, tenant)
return err
}
// WorkflowRunList converts echo context to params.
func (w *ServerInterfaceWrapper) WorkflowRunList(ctx echo.Context) error {
var err error
@@ -3103,6 +3134,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
router.GET(baseURL+"/api/v1/tenants/:tenant/workflow-runs/:workflow-run", wrapper.WorkflowRunGet)
router.GET(baseURL+"/api/v1/tenants/:tenant/workflow-runs/:workflow-run/prs", wrapper.WorkflowRunListPullRequests)
router.GET(baseURL+"/api/v1/tenants/:tenant/workflows", wrapper.WorkflowList)
router.POST(baseURL+"/api/v1/tenants/:tenant/workflows/cancel", wrapper.WorkflowRunCancel)
router.GET(baseURL+"/api/v1/tenants/:tenant/workflows/runs", wrapper.WorkflowRunList)
router.GET(baseURL+"/api/v1/tenants/:tenant/workflows/runs/metrics", wrapper.WorkflowRunGetMetrics)
router.GET(baseURL+"/api/v1/users/current", wrapper.UserGetCurrent)
@@ -4862,6 +4894,44 @@ func (response WorkflowList403JSONResponse) VisitWorkflowListResponse(w http.Res
return json.NewEncoder(w).Encode(response)
}
type WorkflowRunCancelRequestObject struct {
Tenant openapi_types.UUID `json:"tenant"`
Body *WorkflowRunCancelJSONRequestBody
}
type WorkflowRunCancelResponseObject interface {
VisitWorkflowRunCancelResponse(w http.ResponseWriter) error
}
type WorkflowRunCancel200JSONResponse struct {
WorkflowRunIds *[]openapi_types.UUID `json:"workflowRunIds,omitempty"`
}
func (response WorkflowRunCancel200JSONResponse) VisitWorkflowRunCancelResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type WorkflowRunCancel400JSONResponse APIErrors
func (response WorkflowRunCancel400JSONResponse) VisitWorkflowRunCancelResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(400)
return json.NewEncoder(w).Encode(response)
}
type WorkflowRunCancel403JSONResponse APIErrors
func (response WorkflowRunCancel403JSONResponse) VisitWorkflowRunCancelResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(403)
return json.NewEncoder(w).Encode(response)
}
type WorkflowRunListRequestObject struct {
Tenant openapi_types.UUID `json:"tenant"`
Params WorkflowRunListParams
@@ -5869,6 +5939,8 @@ type StrictServerInterface interface {
WorkflowList(ctx echo.Context, request WorkflowListRequestObject) (WorkflowListResponseObject, error)
WorkflowRunCancel(ctx echo.Context, request WorkflowRunCancelRequestObject) (WorkflowRunCancelResponseObject, error)
WorkflowRunList(ctx echo.Context, request WorkflowRunListRequestObject) (WorkflowRunListResponseObject, error)
WorkflowRunGetMetrics(ctx echo.Context, request WorkflowRunGetMetricsRequestObject) (WorkflowRunGetMetricsResponseObject, error)
@@ -7175,6 +7247,37 @@ func (sh *strictHandler) WorkflowList(ctx echo.Context, tenant openapi_types.UUI
return nil
}
// WorkflowRunCancel operation middleware
func (sh *strictHandler) WorkflowRunCancel(ctx echo.Context, tenant openapi_types.UUID) error {
var request WorkflowRunCancelRequestObject
request.Tenant = tenant
var body WorkflowRunCancelJSONRequestBody
if err := ctx.Bind(&body); err != nil {
return err
}
request.Body = &body
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
return sh.ssi.WorkflowRunCancel(ctx, request.(WorkflowRunCancelRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "WorkflowRunCancel")
}
response, err := handler(ctx, request)
if err != nil {
return err
} else if validResponse, ok := response.(WorkflowRunCancelResponseObject); ok {
return validResponse.VisitWorkflowRunCancelResponse(ctx.Response())
} else if response != nil {
return fmt.Errorf("Unexpected response type: %T", response)
}
return nil
}
// WorkflowRunList operation middleware
func (sh *strictHandler) WorkflowRunList(ctx echo.Context, tenant openapi_types.UUID, params WorkflowRunListParams) error {
var request WorkflowRunListRequestObject
@@ -7844,155 +7947,157 @@ func (sh *strictHandler) WorkflowVersionGetDefinition(ctx echo.Context, workflow
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
"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",
}
// GetSwagger returns the content of the embedded swagger specification file

View File

@@ -76,6 +76,7 @@ import {
WorkflowRunList,
WorkflowRunStatus,
WorkflowRunStatusList,
WorkflowRunsCancelRequest,
WorkflowRunsMetrics,
WorkflowVersion,
WorkflowVersionDefinition,
@@ -941,6 +942,30 @@ export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType
format: "json",
...params,
});
/**
* @description Cancel a batch of workflow runs
*
* @tags Workflow Run
* @name WorkflowRunCancel
* @summary Cancel workflow runs
* @request POST:/api/v1/tenants/{tenant}/workflows/cancel
* @secure
*/
workflowRunCancel = (tenant: string, data: WorkflowRunsCancelRequest, params: RequestParams = {}) =>
this.request<
{
workflowRunIds?: string[];
},
APIErrors
>({
path: `/api/v1/tenants/${tenant}/workflows/cancel`,
method: "POST",
body: data,
secure: true,
type: ContentType.Json,
format: "json",
...params,
});
/**
* @description Get a workflow for a tenant
*

View File

@@ -589,6 +589,10 @@ export enum WorkflowRunStatus {
export type WorkflowRunStatusList = WorkflowRunStatus[];
export interface WorkflowRunsCancelRequest {
workflowRunIds: string[];
}
export enum JobRunStatus {
PENDING = "PENDING",
RUNNING = "RUNNING",

View File

@@ -1,7 +1,12 @@
import { Separator } from '@/components/ui/separator';
import { StepRun, StepRunStatus, queries } from '@/lib/api';
import api, {
StepRun,
StepRunStatus,
WorkflowRunStatus,
queries,
} from '@/lib/api';
import CronPrettifier from 'cronstrue';
import { useQuery } from '@tanstack/react-query';
import { useMutation, useQuery } from '@tanstack/react-query';
import { Link, useOutletContext, useParams } from 'react-router-dom';
import invariant from 'tiny-invariant';
import { timeBetween } from '@/lib/utils';
@@ -13,6 +18,22 @@ import { StepRunPlayground } from './components/step-run-playground';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import RelativeDate from '@/components/molecules/relative-date';
import { RunStatus } from '../components/run-statuses';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { BiDotsVertical } from 'react-icons/bi';
import { useApiError } from '@/lib/hooks';
export const WORKFLOW_RUN_TERMINAL_STATUSES = [
WorkflowRunStatus.CANCELLED,
WorkflowRunStatus.FAILED,
WorkflowRunStatus.SUCCEEDED,
];
export default function ExpandedWorkflowRun() {
const [selectedStepRun, setSelectedStepRun] = useState<StepRun | undefined>();
@@ -68,6 +89,30 @@ export default function ExpandedWorkflowRun() {
}
}, [runQuery.data, params.run, selectedStepRun]);
const { handleApiError } = useApiError({});
const cancelWorkflowRunMutation = useMutation({
mutationKey: [
'workflow-run:cancel',
runQuery?.data?.tenantId,
runQuery?.data?.metadata.id,
],
mutationFn: async () => {
const tenantId = runQuery?.data?.tenantId;
const workflowRunId = runQuery?.data?.metadata.id;
invariant(tenantId, 'has tenantId');
invariant(workflowRunId, 'has tenantId');
const res = await api.workflowRunCancel(tenantId, {
workflowRunIds: [workflowRunId],
});
return res.data;
},
onError: handleApiError,
});
if (runQuery.isLoading || !runQuery.data) {
return <Loading />;
}
@@ -105,7 +150,34 @@ export default function ExpandedWorkflowRun() {
/{selectedStepRun?.step?.readableId || '*'}
</h2>
</div>
<RunStatus status={run.status} className="text-sm mt-1 px-4 shrink" />
<div className="flex flex-row gap-2 items-center">
<RunStatus
status={run.status}
className="text-sm mt-1 px-4 shrink"
/>
<DropdownMenu>
<DropdownMenuTrigger>
<Button
aria-label="Workflow Actions"
size="icon"
variant="outline"
>
<BiDotsVertical />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
disabled={WORKFLOW_RUN_TERMINAL_STATUSES.includes(run.status)}
onClick={() => {
cancelWorkflowRunMutation.mutate();
}}
>
Cancel all running steps
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="flex flex-row justify-start items-center gap-2">
<div className="text-sm text-gray-700 dark:text-gray-300">

View File

@@ -5,8 +5,33 @@ import { Link } from 'react-router-dom';
import { RunStatus } from './run-statuses';
import { AdditionalMetadata } from '../../events/components/additional-metadata';
import RelativeDate from '@/components/molecules/relative-date';
import { Checkbox } from '@/components/ui/checkbox';
export const columns: ColumnDef<WorkflowRun>[] = [
{
id: 'select',
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && 'indeterminate')
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
className="translate-y-[2px]"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
className="translate-y-[2px]"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: 'id',
header: ({ column }) => (

View File

@@ -4,12 +4,13 @@ import { useMemo, useState } from 'react';
import {
ColumnFiltersState,
PaginationState,
RowSelectionState,
SortingState,
VisibilityState,
} from '@tanstack/react-table';
import { useQuery } from '@tanstack/react-query';
import { useMutation, useQuery } from '@tanstack/react-query';
import invariant from 'tiny-invariant';
import { WorkflowRunStatus, queries } from '@/lib/api';
import api, { WorkflowRunStatus, queries } from '@/lib/api';
import { Loading } from '@/components/ui/loading.tsx';
import { TenantContextType } from '@/lib/outlet';
import { useOutletContext } from 'react-router-dom';
@@ -19,8 +20,10 @@ import {
ToolbarType,
} from '@/components/molecules/data-table/data-table-toolbar';
import { Button } from '@/components/ui/button';
import { ArrowPathIcon } from '@heroicons/react/24/outline';
import { ArrowPathIcon, XMarkIcon } from '@heroicons/react/24/outline';
import { WorkflowRunsMetricsView } from './workflow-runs-metrics';
import queryClient from '@/query-client';
import { useApiError } from '@/lib/hooks';
export interface WorkflowRunsTableProps {
workflowId?: string;
@@ -127,6 +130,39 @@ export function WorkflowRunsTable({
...queries.workflows.list(tenant.metadata.id),
});
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const selectedRuns = useMemo(() => {
return Object.entries(rowSelection)
.filter(([_, selected]) => !!selected)
.map(([id]) => (listWorkflowRunsQuery.data?.rows || [])[Number(id)]);
}, [listWorkflowRunsQuery.data?.rows, rowSelection]);
const { handleApiError } = useApiError({});
const cancelWorkflowRunMutation = useMutation({
mutationKey: ['workflow-run:cancel', tenant.metadata.id, selectedRuns],
mutationFn: async () => {
const tenantId = tenant.metadata.id;
const workflowRunIds = selectedRuns.map((wr) => wr.metadata.id);
invariant(tenantId, 'has tenantId');
invariant(workflowRunIds, 'has runIds');
const res = await api.workflowRunCancel(tenantId, {
workflowRunIds,
});
return res.data;
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: queries.workflowRuns.list(tenant.metadata.id, {}).queryKey,
});
},
onError: handleApiError,
});
const workflowKeyFilters = useMemo((): FilterOption[] => {
return (
workflowKeys?.rows?.map((key) => ({
@@ -188,6 +224,20 @@ export function WorkflowRunsTable({
};
const actions = [
<Button
disabled={!Object.values(rowSelection).some((selected) => !!selected)}
key="cancel"
className="h-8 px-2 lg:px-3"
size="sm"
onClick={() => {
cancelWorkflowRunMutation.mutate();
}}
variant={'outline'}
aria-label="Cancel Selected Runs"
>
<XMarkIcon className={`mr-2 h-4 w-4 transition-transform`} />
Cancel Selected Runs
</Button>,
<Button
key="refresh"
className="h-8 px-2 lg:px-3"
@@ -232,6 +282,8 @@ export function WorkflowRunsTable({
pagination={pagination}
setPagination={setPagination}
onSetPageSize={setPageSize}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
pageCount={listWorkflowRunsQuery.data?.pagination?.num_pages || 0}
/>
</>

View File

@@ -320,11 +320,17 @@ func (ec *JobsControllerImpl) handleJobRunCancelled(ctx context.Context, task *m
for _, stepRun := range stepRuns {
stepRunCp := stepRun
reason := "JOB_RUN_CANCELLED"
if payload.Reason != nil {
reason = *payload.Reason
}
g.Go(func() error {
return ec.mq.AddMessage(
ctx,
msgqueue.JOB_PROCESSING_QUEUE,
tasktypes.StepRunCancelToTask(stepRunCp, "JOB_RUN_CANCELLED"),
tasktypes.StepRunCancelToTask(stepRunCp, reason),
)
})
}

View File

@@ -152,7 +152,7 @@ func (wc *WorkflowsControllerImpl) handleWorkflowRunFinished(ctx context.Context
err = wc.mq.AddMessage(
ctx,
msgqueue.JOB_PROCESSING_QUEUE,
tasktypes.JobRunCancelledToTask(metadata.TenantId, sqlchelpers.UUIDToStr(jobRun.ID)),
tasktypes.JobRunCancelledToTask(metadata.TenantId, sqlchelpers.UUIDToStr(jobRun.ID), nil),
)
if err != nil {
@@ -319,7 +319,7 @@ func (wc *WorkflowsControllerImpl) cancelWorkflowRunJobs(ctx context.Context, wo
err := wc.mq.AddMessage(
context.Background(),
msgqueue.JOB_PROCESSING_QUEUE,
tasktypes.JobRunCancelledToTask(tenantId, jobRunId),
tasktypes.JobRunCancelledToTask(tenantId, jobRunId, nil),
)
if err != nil {

View File

@@ -31,16 +31,18 @@ func JobRunQueuedToTask(tenantId, jobRunId string) *msgqueue.Message {
}
type JobRunCancelledTaskPayload struct {
JobRunId string `json:"job_run_id" validate:"required,uuid"`
JobRunId string `json:"job_run_id" validate:"required,uuid"`
Reason *string `json:"reason,omitempty"`
}
type JobRunCancelledTaskMetadata struct {
TenantId string `json:"tenant_id" validate:"required,uuid"`
}
func JobRunCancelledToTask(tenantId, jobRunId string) *msgqueue.Message {
func JobRunCancelledToTask(tenantId, jobRunId string, reason *string) *msgqueue.Message {
payload, _ := datautils.ToJSONMap(JobRunCancelledTaskPayload{
JobRunId: jobRunId,
Reason: reason,
})
metadata, _ := datautils.ToJSONMap(JobRunCancelledTaskMetadata{

View File

@@ -942,6 +942,11 @@ type WorkflowRunTriggeredBy struct {
ParentId string `json:"parentId"`
}
// WorkflowRunsCancelRequest defines model for WorkflowRunsCancelRequest.
type WorkflowRunsCancelRequest struct {
WorkflowRunIds []openapi_types.UUID `json:"workflowRunIds"`
}
// WorkflowRunsMetrics defines model for WorkflowRunsMetrics.
type WorkflowRunsMetrics struct {
Counts *WorkflowRunsMetricsCounts `json:"counts,omitempty"`
@@ -1189,6 +1194,9 @@ type SnsCreateJSONRequestBody = CreateSNSIntegrationRequest
// StepRunUpdateRerunJSONRequestBody defines body for StepRunUpdateRerun for application/json ContentType.
type StepRunUpdateRerunJSONRequestBody = RerunStepRunRequest
// WorkflowRunCancelJSONRequestBody defines body for WorkflowRunCancel for application/json ContentType.
type WorkflowRunCancelJSONRequestBody = WorkflowRunsCancelRequest
// TenantInviteAcceptJSONRequestBody defines body for TenantInviteAccept for application/json ContentType.
type TenantInviteAcceptJSONRequestBody = AcceptInviteRequest
@@ -1446,6 +1454,11 @@ type ClientInterface interface {
// WorkflowList request
WorkflowList(ctx context.Context, tenant openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error)
// WorkflowRunCancelWithBody request with any body
WorkflowRunCancelWithBody(ctx context.Context, tenant openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
WorkflowRunCancel(ctx context.Context, tenant openapi_types.UUID, body WorkflowRunCancelJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
// WorkflowRunList request
WorkflowRunList(ctx context.Context, tenant openapi_types.UUID, params *WorkflowRunListParams, reqEditors ...RequestEditorFn) (*http.Response, error)
@@ -2235,6 +2248,30 @@ func (c *Client) WorkflowList(ctx context.Context, tenant openapi_types.UUID, re
return c.Client.Do(req)
}
func (c *Client) WorkflowRunCancelWithBody(ctx context.Context, tenant openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewWorkflowRunCancelRequestWithBody(c.Server, tenant, contentType, body)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
return nil, err
}
return c.Client.Do(req)
}
func (c *Client) WorkflowRunCancel(ctx context.Context, tenant openapi_types.UUID, body WorkflowRunCancelJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewWorkflowRunCancelRequest(c.Server, tenant, body)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
return nil, err
}
return c.Client.Do(req)
}
func (c *Client) WorkflowRunList(ctx context.Context, tenant openapi_types.UUID, params *WorkflowRunListParams, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewWorkflowRunListRequest(c.Server, tenant, params)
if err != nil {
@@ -4712,6 +4749,53 @@ func NewWorkflowListRequest(server string, tenant openapi_types.UUID) (*http.Req
return req, nil
}
// NewWorkflowRunCancelRequest calls the generic WorkflowRunCancel builder with application/json body
func NewWorkflowRunCancelRequest(server string, tenant openapi_types.UUID, body WorkflowRunCancelJSONRequestBody) (*http.Request, error) {
var bodyReader io.Reader
buf, err := json.Marshal(body)
if err != nil {
return nil, err
}
bodyReader = bytes.NewReader(buf)
return NewWorkflowRunCancelRequestWithBody(server, tenant, "application/json", bodyReader)
}
// NewWorkflowRunCancelRequestWithBody generates requests for WorkflowRunCancel with any type of body
func NewWorkflowRunCancelRequestWithBody(server string, tenant openapi_types.UUID, contentType string, body io.Reader) (*http.Request, error) {
var err error
var pathParam0 string
pathParam0, err = runtime.StyleParamWithLocation("simple", false, "tenant", runtime.ParamLocationPath, tenant)
if err != nil {
return nil, err
}
serverURL, err := url.Parse(server)
if err != nil {
return nil, err
}
operationPath := fmt.Sprintf("/api/v1/tenants/%s/workflows/cancel", pathParam0)
if operationPath[0] == '/' {
operationPath = "." + operationPath
}
queryURL, err := serverURL.Parse(operationPath)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", queryURL.String(), body)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", contentType)
return req, nil
}
// NewWorkflowRunListRequest generates requests for WorkflowRunList
func NewWorkflowRunListRequest(server string, tenant openapi_types.UUID, params *WorkflowRunListParams) (*http.Request, error) {
var err error
@@ -6105,6 +6189,11 @@ type ClientWithResponsesInterface interface {
// WorkflowListWithResponse request
WorkflowListWithResponse(ctx context.Context, tenant openapi_types.UUID, reqEditors ...RequestEditorFn) (*WorkflowListResponse, error)
// WorkflowRunCancelWithBodyWithResponse request with any body
WorkflowRunCancelWithBodyWithResponse(ctx context.Context, tenant openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*WorkflowRunCancelResponse, error)
WorkflowRunCancelWithResponse(ctx context.Context, tenant openapi_types.UUID, body WorkflowRunCancelJSONRequestBody, reqEditors ...RequestEditorFn) (*WorkflowRunCancelResponse, error)
// WorkflowRunListWithResponse request
WorkflowRunListWithResponse(ctx context.Context, tenant openapi_types.UUID, params *WorkflowRunListParams, reqEditors ...RequestEditorFn) (*WorkflowRunListResponse, error)
@@ -7322,6 +7411,32 @@ func (r WorkflowListResponse) StatusCode() int {
return 0
}
type WorkflowRunCancelResponse struct {
Body []byte
HTTPResponse *http.Response
JSON200 *struct {
WorkflowRunIds *[]openapi_types.UUID `json:"workflowRunIds,omitempty"`
}
JSON400 *APIErrors
JSON403 *APIErrors
}
// Status returns HTTPResponse.Status
func (r WorkflowRunCancelResponse) Status() string {
if r.HTTPResponse != nil {
return r.HTTPResponse.Status
}
return http.StatusText(0)
}
// StatusCode returns HTTPResponse.StatusCode
func (r WorkflowRunCancelResponse) StatusCode() int {
if r.HTTPResponse != nil {
return r.HTTPResponse.StatusCode
}
return 0
}
type WorkflowRunListResponse struct {
Body []byte
HTTPResponse *http.Response
@@ -8444,6 +8559,23 @@ func (c *ClientWithResponses) WorkflowListWithResponse(ctx context.Context, tena
return ParseWorkflowListResponse(rsp)
}
// WorkflowRunCancelWithBodyWithResponse request with arbitrary body returning *WorkflowRunCancelResponse
func (c *ClientWithResponses) WorkflowRunCancelWithBodyWithResponse(ctx context.Context, tenant openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*WorkflowRunCancelResponse, error) {
rsp, err := c.WorkflowRunCancelWithBody(ctx, tenant, contentType, body, reqEditors...)
if err != nil {
return nil, err
}
return ParseWorkflowRunCancelResponse(rsp)
}
func (c *ClientWithResponses) WorkflowRunCancelWithResponse(ctx context.Context, tenant openapi_types.UUID, body WorkflowRunCancelJSONRequestBody, reqEditors ...RequestEditorFn) (*WorkflowRunCancelResponse, error) {
rsp, err := c.WorkflowRunCancel(ctx, tenant, body, reqEditors...)
if err != nil {
return nil, err
}
return ParseWorkflowRunCancelResponse(rsp)
}
// WorkflowRunListWithResponse request returning *WorkflowRunListResponse
func (c *ClientWithResponses) WorkflowRunListWithResponse(ctx context.Context, tenant openapi_types.UUID, params *WorkflowRunListParams, reqEditors ...RequestEditorFn) (*WorkflowRunListResponse, error) {
rsp, err := c.WorkflowRunList(ctx, tenant, params, reqEditors...)
@@ -10577,6 +10709,48 @@ func ParseWorkflowListResponse(rsp *http.Response) (*WorkflowListResponse, error
return response, nil
}
// ParseWorkflowRunCancelResponse parses an HTTP response from a WorkflowRunCancelWithResponse call
func ParseWorkflowRunCancelResponse(rsp *http.Response) (*WorkflowRunCancelResponse, error) {
bodyBytes, err := io.ReadAll(rsp.Body)
defer func() { _ = rsp.Body.Close() }()
if err != nil {
return nil, err
}
response := &WorkflowRunCancelResponse{
Body: bodyBytes,
HTTPResponse: rsp,
}
switch {
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
var dest struct {
WorkflowRunIds *[]openapi_types.UUID `json:"workflowRunIds,omitempty"`
}
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON200 = &dest
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
var dest APIErrors
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON400 = &dest
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
var dest APIErrors
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON403 = &dest
}
return response, nil
}
// ParseWorkflowRunListResponse parses an HTTP response from a WorkflowRunListWithResponse call
func ParseWorkflowRunListResponse(rsp *http.Response) (*WorkflowRunListResponse, error) {
bodyBytes, err := io.ReadAll(rsp.Body)