From bc6fc8ba131e4cb00fc67f519abe82a735500319 Mon Sep 17 00:00:00 2001 From: SCcagg5 Date: Sat, 31 Oct 2026 11:35:50 +0100 Subject: [PATCH 1/2] api: Add step samples, events, availability and selected-step runs (#3384) * Intake, CLI and REST API: samples, events and availability of a step, selected step runs; generated types * Satisfy linters in the step samples API * Name the output source in the start steps test * Skip POSIX-shell step run tests on Windows * Clarify API descriptions of samples and sources * Answer step sample queries by the contract * Make the spec hash check atomic with the write * Keep empty param defaults in the reference scope * Tighten the outputsFromRuns conformance test * Describe --outputs-from as dependency carrying * Keep released step run contracts and fix reuse * Mark handler output lists as static in the scope * Keep the logs of a source run when removing a run --- api/v1/api.gen.go | 4962 +++++++++++------ api/v1/api.yaml | 729 ++- conformance/cli/start_test.go | 35 +- .../spec013_step_run/ephemeral_test.go | 482 ++ .../spec013_step_run/from_runs_test.go | 208 + conformance/spec013_step_run/handlers_test.go | 119 + .../spec021_mcp_read_tool/read_run_test.go | 34 + .../spec083_continuous_runs/api_test.go | 281 + .../status_log_test.go | 84 + .../spec083_continuous_runs/testdata/beat.sh | 3 + .../testdata/continuous_until_done.yaml | 8 + .../testdata/limited_until_done.yaml | 9 + internal/cmd/remote_commands.go | 5 + internal/cmd/remote_commands_test.go | 7 +- internal/cmd/start.go | 26 +- internal/cmd/start_steps.go | 60 +- internal/cmd/start_test.go | 138 +- internal/intake/seed.go | 14 +- internal/intake/steps.go | 354 +- internal/intake/steps_test.go | 337 +- internal/persis/dag.go | 5 + internal/persis/dag_repository.go | 23 +- internal/persis/dag_repository_test.go | 4 + internal/persis/file/dag/store.go | 67 + internal/persis/file/dag/store_test.go | 74 + internal/persis/file/dagrun/dagrun.go | 22 +- internal/persis/file/dagrun/dagrun_test.go | 52 + internal/samples/reader.go | 27 +- internal/samples/store_test.go | 11 +- internal/service/frontend/api/v1/api.go | 3 + internal/service/frontend/api/v1/dagruns.go | 48 +- .../frontend/api/v1/dagruns_edit_retry.go | 3 + internal/service/frontend/api/v1/dags.go | 76 +- internal/service/frontend/api/v1/dags_test.go | 156 + .../service/frontend/api/v1/editor_issues.go | 731 +++ .../frontend/api/v1/editor_issues_api_test.go | 134 + .../frontend/api/v1/editor_issues_test.go | 536 ++ .../service/frontend/api/v1/editor_scope.go | 152 + .../frontend/api/v1/editor_scope_api_test.go | 117 + .../frontend/api/v1/editor_scope_steps.go | 308 + .../api/v1/editor_scope_steps_test.go | 587 ++ .../frontend/api/v1/editor_scope_test.go | 336 ++ .../frontend/api/v1/editor_step_outputs.go | 163 + .../api/v1/editor_step_outputs_test.go | 416 ++ .../service/frontend/api/v1/seeded_run.go | 25 +- .../service/frontend/api/v1/start_steps.go | 142 +- .../api/v1/start_steps_internal_test.go | 178 +- .../service/frontend/api/v1/step_samples.go | 283 + .../frontend/api/v1/step_samples_test.go | 409 ++ .../service/frontend/api/v1/transformer.go | 42 + .../service/frontend/ephemeral_cleanup.go | 73 + internal/service/frontend/server.go | 4 + llms.txt | 5 +- skills/dagu/references/cli.md | 5 +- specs/013-step-run.md | 8 +- specs/021-mcp-read-tool.md | 2 + specs/083-continuous-runs.md | 31 + ui/src/api/v1/schema.ts | 655 ++- 58 files changed, 11911 insertions(+), 1897 deletions(-) create mode 100644 conformance/spec013_step_run/ephemeral_test.go create mode 100644 conformance/spec013_step_run/from_runs_test.go create mode 100644 conformance/spec013_step_run/handlers_test.go create mode 100644 conformance/spec083_continuous_runs/api_test.go create mode 100644 conformance/spec083_continuous_runs/status_log_test.go create mode 100644 conformance/spec083_continuous_runs/testdata/beat.sh create mode 100644 conformance/spec083_continuous_runs/testdata/continuous_until_done.yaml create mode 100644 conformance/spec083_continuous_runs/testdata/limited_until_done.yaml create mode 100644 internal/service/frontend/api/v1/editor_issues.go create mode 100644 internal/service/frontend/api/v1/editor_issues_api_test.go create mode 100644 internal/service/frontend/api/v1/editor_issues_test.go create mode 100644 internal/service/frontend/api/v1/editor_scope.go create mode 100644 internal/service/frontend/api/v1/editor_scope_api_test.go create mode 100644 internal/service/frontend/api/v1/editor_scope_steps.go create mode 100644 internal/service/frontend/api/v1/editor_scope_steps_test.go create mode 100644 internal/service/frontend/api/v1/editor_scope_test.go create mode 100644 internal/service/frontend/api/v1/editor_step_outputs.go create mode 100644 internal/service/frontend/api/v1/editor_step_outputs_test.go create mode 100644 internal/service/frontend/api/v1/step_samples.go create mode 100644 internal/service/frontend/api/v1/step_samples_test.go create mode 100644 internal/service/frontend/ephemeral_cleanup.go diff --git a/api/v1/api.gen.go b/api/v1/api.gen.go index 280e7150ec..fc542a8bbe 100644 --- a/api/v1/api.gen.go +++ b/api/v1/api.gen.go @@ -257,6 +257,30 @@ func (e ArtifactPreviewKind) Valid() bool { } } +// Defines values for AvailabilityWindowWindow. +const ( + AvailabilityWindowWindowN24h AvailabilityWindowWindow = "24h" + AvailabilityWindowWindowN30d AvailabilityWindowWindow = "30d" + AvailabilityWindowWindowN365d AvailabilityWindowWindow = "365d" + AvailabilityWindowWindowN7d AvailabilityWindowWindow = "7d" +) + +// Valid indicates whether the value is a known member of the AvailabilityWindowWindow enum. +func (e AvailabilityWindowWindow) Valid() bool { + switch e { + case AvailabilityWindowWindowN24h: + return true + case AvailabilityWindowWindowN30d: + return true + case AvailabilityWindowWindowN365d: + return true + case AvailabilityWindowWindowN7d: + return true + default: + return false + } +} + // Defines values for BuildExecutionDecision. const ( BuildExecutionDecisionAlways BuildExecutionDecision = "always" @@ -1073,6 +1097,48 @@ func (e QueueType) Valid() bool { } } +// Defines values for ReferenceScopeOutputSource. +const ( + ReferenceScopeOutputSourceAction ReferenceScopeOutputSource = "action" + ReferenceScopeOutputSourceDeclared ReferenceScopeOutputSource = "declared" + ReferenceScopeOutputSourceOutputVariable ReferenceScopeOutputSource = "outputVariable" + ReferenceScopeOutputSourceStdoutField ReferenceScopeOutputSource = "stdoutField" +) + +// Valid indicates whether the value is a known member of the ReferenceScopeOutputSource enum. +func (e ReferenceScopeOutputSource) Valid() bool { + switch e { + case ReferenceScopeOutputSourceAction: + return true + case ReferenceScopeOutputSourceDeclared: + return true + case ReferenceScopeOutputSourceOutputVariable: + return true + case ReferenceScopeOutputSourceStdoutField: + return true + default: + return false + } +} + +// Defines values for ReferenceScopeStepOutputsKnown. +const ( + ReferenceScopeStepOutputsKnownRuntime ReferenceScopeStepOutputsKnown = "runtime" + ReferenceScopeStepOutputsKnownStatic ReferenceScopeStepOutputsKnown = "static" +) + +// Valid indicates whether the value is a known member of the ReferenceScopeStepOutputsKnown enum. +func (e ReferenceScopeStepOutputsKnown) Valid() bool { + switch e { + case ReferenceScopeStepOutputsKnownRuntime: + return true + case ReferenceScopeStepOutputsKnownStatic: + return true + default: + return false + } +} + // Defines values for RemoteNodeResponseAuthType. const ( RemoteNodeResponseAuthTypeBasic RemoteNodeResponseAuthType = "basic" @@ -1130,6 +1196,24 @@ func (e RepeatMode) Valid() bool { } } +// Defines values for ReusedInputVia. +const ( + ReusedInputViaCache ReusedInputVia = "cache" + ReusedInputViaRun ReusedInputVia = "run" +) + +// Valid indicates whether the value is a known member of the ReusedInputVia enum. +func (e ReusedInputVia) Valid() bool { + switch e { + case ReusedInputViaCache: + return true + case ReusedInputViaRun: + return true + default: + return false + } +} + // Defines values for RunDateMode. const ( RunDateModeCustom RunDateMode = "custom" @@ -1409,6 +1493,30 @@ func (e StatusLabel) Valid() bool { } } +// Defines values for StepEventKind. +const ( + StepEventKindFailed StepEventKind = "failed" + StepEventKindRecovered StepEventKind = "recovered" + StepEventKindStarted StepEventKind = "started" + StepEventKindStopped StepEventKind = "stopped" +) + +// Valid indicates whether the value is a known member of the StepEventKind enum. +func (e StepEventKind) Valid() bool { + switch e { + case StepEventKindFailed: + return true + case StepEventKindRecovered: + return true + case StepEventKindStarted: + return true + case StepEventKindStopped: + return true + default: + return false + } +} + // Defines values for StepOutputDeclarationType. const ( StepOutputDeclarationTypeJson StepOutputDeclarationType = "json" @@ -1724,6 +1832,51 @@ func (e UserRole) Valid() bool { } } +// Defines values for ValidationIssueSeverity. +const ( + ValidationIssueSeverityError ValidationIssueSeverity = "error" + ValidationIssueSeverityNotice ValidationIssueSeverity = "notice" + ValidationIssueSeverityWarning ValidationIssueSeverity = "warning" +) + +// Valid indicates whether the value is a known member of the ValidationIssueSeverity enum. +func (e ValidationIssueSeverity) Valid() bool { + switch e { + case ValidationIssueSeverityError: + return true + case ValidationIssueSeverityNotice: + return true + case ValidationIssueSeverityWarning: + return true + default: + return false + } +} + +// Defines values for ValidationIssueSource. +const ( + ValidationIssueSourceBuild ValidationIssueSource = "build" + ValidationIssueSourceLoad ValidationIssueSource = "load" + ValidationIssueSourceValueReference ValidationIssueSource = "valueReference" + ValidationIssueSourceWarning ValidationIssueSource = "warning" +) + +// Valid indicates whether the value is a known member of the ValidationIssueSource enum. +func (e ValidationIssueSource) Valid() bool { + switch e { + case ValidationIssueSourceBuild: + return true + case ValidationIssueSourceLoad: + return true + case ValidationIssueSourceValueReference: + return true + case ValidationIssueSourceWarning: + return true + default: + return false + } +} + // Defines values for ValueReferenceNoticeClass. const ( ValueReferenceNoticeClassDefect ValueReferenceNoticeClass = "defect" @@ -2289,7 +2442,7 @@ type AgentEvent struct { // ChildDagRunId Child DAG-run this action produced, for linking to its run page. Absent for steps that run no child DAG. ChildDagRunId *string `json:"childDagRunId,omitempty"` - // FinishedAt RFC3339 timestamp when the step finished + // FinishedAt RFC 3339 timestamp with millisecond precision when the step finished FinishedAt *string `json:"finishedAt,omitempty"` // Kind What the agent did on this turn @@ -2301,7 +2454,7 @@ type AgentEvent struct { // Reason Agent's justification, or why the call was rejected Reason *string `json:"reason,omitempty"` - // StartedAt RFC3339 timestamp when the step started + // StartedAt RFC 3339 timestamp with millisecond precision when the step started StartedAt *string `json:"startedAt,omitempty"` // Status Resulting step status for an action event, or the new task status for a task_status event @@ -2646,6 +2799,18 @@ type AuditLogsResponse struct { Total int `json:"total"` } +// AvailabilityWindow defines model for AvailabilityWindow. +type AvailabilityWindow struct { + // Availability From 0 to 1. Absent when `count` is 0. + Availability *float64 `json:"availability,omitempty"` + Count int64 `json:"count"` + Failures int64 `json:"failures"` + Window AvailabilityWindowWindow `json:"window"` +} + +// AvailabilityWindowWindow defines model for AvailabilityWindow.Window. +type AvailabilityWindowWindow string + // BuildExecution Build execution decision and its explanation type BuildExecution struct { Decision BuildExecutionDecision `json:"decision"` @@ -3345,10 +3510,13 @@ type DAGRunDetails struct { // DagRunId Unique identifier for the DAG-run. The special value 'latest' can be used to reference the most recent DAG-run. DagRunId DAGRunId `json:"dagRunId"` + // Ephemeral True for an ephemeral run (a test run that leaves no trace). Absent or false for other runs. Only the DAG history with includeEphemeral and the run by ID return such a run. + Ephemeral *bool `json:"ephemeral,omitempty"` + // Error Top-level error recorded for the DAG-run, such as a definition build failure that prevented the run from starting Error *string `json:"error,omitempty"` - // FinishedAt RFC 3339 timestamp when the DAG-run finished + // FinishedAt RFC 3339 timestamp with millisecond precision when the DAG-run finished. A run that an older version stored has second precision. FinishedAt string `json:"finishedAt"` // HumanTaskResumePending Whether completed human-task input is durable but the same DAG-run still needs its retry queued @@ -3407,8 +3575,11 @@ type DAGRunDetails struct { ProfileName *RuntimeProfileName `json:"profileName,omitempty"` // QueuedAt RFC 3339 timestamp when the DAG-run was queued - QueuedAt *string `json:"queuedAt,omitempty"` - RootDAGRunId string `json:"rootDAGRunId"` + QueuedAt *string `json:"queuedAt,omitempty"` + + // ReusedInputs Where the carried outputs of the skipped steps of a selected-steps run come from, one entry per skipped step, in DAG step order. Absent when the run carried no outputs. + ReusedInputs *[]ReusedInput `json:"reusedInputs,omitempty"` + RootDAGRunId string `json:"rootDAGRunId"` // RootDAGRunName Name of the root DAG-run RootDAGRunName string `json:"rootDAGRunName"` @@ -3422,7 +3593,7 @@ type DAGRunDetails struct { // SpecFromFile Whether this DAG-run still has a usable source file on disk, so reschedule can load the current spec from that file instead of the stored historical YAML snapshot. SpecFromFile *bool `json:"specFromFile,omitempty"` - // StartedAt RFC 3339 timestamp when the DAG-run started + // StartedAt RFC 3339 timestamp with millisecond precision when the DAG-run started. A run that an older version stored has second precision. StartedAt string `json:"startedAt"` // Status Numeric status code indicating current DAG-run state: @@ -3495,7 +3666,10 @@ type DAGRunSummary struct { // DagRunId Unique identifier for the DAG-run. The special value 'latest' can be used to reference the most recent DAG-run. DagRunId DAGRunId `json:"dagRunId"` - // FinishedAt RFC 3339 timestamp when the DAG-run finished + // Ephemeral True for an ephemeral run (a test run that leaves no trace). Absent or false for other runs. Only the DAG history with includeEphemeral and the run by ID return such a run. + Ephemeral *bool `json:"ephemeral,omitempty"` + + // FinishedAt RFC 3339 timestamp with millisecond precision when the DAG-run finished. A run that an older version stored has second precision. FinishedAt string `json:"finishedAt"` // Labels List of labels for categorizing and filtering DAG runs @@ -3522,7 +3696,7 @@ type DAGRunSummary struct { // ScheduleTime RFC 3339 timestamp of when the DAG-run was scheduled to run ScheduleTime *string `json:"scheduleTime,omitempty"` - // StartedAt RFC 3339 timestamp when the DAG-run started + // StartedAt RFC 3339 timestamp with millisecond precision when the DAG-run started. A run that an older version stored has second precision. StartedAt string `json:"startedAt"` // Status Numeric status code indicating current DAG-run state: @@ -3690,7 +3864,9 @@ type ForeachBodyStep struct { // AgentSession Durable state for a managed coding-agent session AgentSession *AgentSession `json:"agentSession,omitempty"` Error *string `json:"error,omitempty"` - FinishedAt *string `json:"finishedAt,omitempty"` + + // FinishedAt RFC 3339 timestamp with millisecond precision when the body step finished + FinishedAt *string `json:"finishedAt,omitempty"` // ForeachParent For a body step that is itself a foreach, the parent path that lists its items ForeachParent *string `json:"foreachParent,omitempty"` @@ -3706,7 +3882,9 @@ type ForeachBodyStep struct { ModelUsage *StepModelUsage `json:"modelUsage,omitempty"` Name string `json:"name"` RetryCount *int `json:"retryCount,omitempty"` - StartedAt *string `json:"startedAt,omitempty"` + + // StartedAt RFC 3339 timestamp with millisecond precision when the body step started + StartedAt *string `json:"startedAt,omitempty"` // Status Numeric status code indicating current node state: // 0: "Not started" @@ -3727,13 +3905,17 @@ type ForeachBodyStep struct { // ForeachItem One item of a foreach step with the status of each body step type ForeachItem struct { - Error *string `json:"error,omitempty"` + Error *string `json:"error,omitempty"` + + // FinishedAt RFC 3339 timestamp with millisecond precision when the body run finished FinishedAt *string `json:"finishedAt,omitempty"` Index int `json:"index"` // Item Path that addresses the item in the foreach item routes - Item string `json:"item"` - Key string `json:"key"` + Item string `json:"item"` + Key string `json:"key"` + + // StartedAt RFC 3339 timestamp with millisecond precision when the body run started StartedAt *string `json:"startedAt,omitempty"` // Status Numeric status code indicating current node state: @@ -3783,7 +3965,7 @@ type ForeachItemSummary struct { // Error Error the body run reported, when it failed Error *string `json:"error,omitempty"` - // FinishedAt RFC3339 timestamp when the body run finished + // FinishedAt RFC 3339 timestamp with millisecond precision when the body run finished FinishedAt *string `json:"finishedAt,omitempty"` Index int `json:"index"` @@ -3791,7 +3973,7 @@ type ForeachItemSummary struct { Item string `json:"item"` Key string `json:"key"` - // StartedAt RFC3339 timestamp when the body run started + // StartedAt RFC 3339 timestamp with millisecond precision when the body run started StartedAt *string `json:"startedAt,omitempty"` // Status Numeric status code indicating current node state: @@ -4482,13 +4664,16 @@ type Node struct { // Build Build execution decision and its explanation Build *BuildExecution `json:"build,omitempty"` + // Continuous True for a continuous step: a step that repeats without a limit. Its executions are recorded as samples (see the samples, events and availability endpoints). + Continuous *bool `json:"continuous,omitempty"` + // DoneCount Number of successful completions for repeating steps DoneCount int `json:"doneCount"` // Error Error message if the step failed Error *string `json:"error,omitempty"` - // FinishedAt RFC3339 timestamp when the step finished + // FinishedAt RFC 3339 timestamp with millisecond precision when the step finished. A step that an older version stored has second precision. FinishedAt string `json:"finishedAt"` // HumanTaskCompletedBy Name of the subject that completed the human task @@ -4521,7 +4706,7 @@ type Node struct { // RetryCount Number of retry attempts made for this step RetryCount int `json:"retryCount"` - // StartedAt RFC3339 timestamp when the step started + // StartedAt RFC 3339 timestamp with millisecond precision when the step started. A step that an older version stored has second precision. StartedAt string `json:"startedAt"` // Status Numeric status code indicating current node state: @@ -5274,6 +5459,62 @@ type QueuesSummary struct { UtilizationPercentage float32 `json:"utilizationPercentage"` } +// ReferenceScope Values that a step of the DAG can reference. +type ReferenceScope struct { + Consts []ReferenceScopeVariable `json:"consts"` + + // Env Environment variable names. Values are not included. + Env []ReferenceScopeVariable `json:"env"` + Params []ReferenceScopeVariable `json:"params"` + + // Secrets Secret names. Values are not included. + Secrets []ReferenceScopeVariable `json:"secrets"` + Steps []ReferenceScopeStep `json:"steps"` +} + +// ReferenceScopeOutput An output of a step that other steps can reference. +type ReferenceScopeOutput struct { + Name string `json:"name"` + + // Reference Reference token, such as ${steps.fetch.outputs.body} or ${RESULT} for an output variable. + Reference string `json:"reference"` + Source ReferenceScopeOutputSource `json:"source"` + Type *string `json:"type,omitempty"` +} + +// ReferenceScopeOutputSource defines model for ReferenceScopeOutput.Source. +type ReferenceScopeOutputSource string + +// ReferenceScopeStep A step and the outputs that other steps can reference. +type ReferenceScopeStep struct { + Id *string `json:"id,omitempty"` + Name string `json:"name"` + Outputs []ReferenceScopeOutput `json:"outputs"` + + // OutputsKnown static when the outputs are known from the spec, runtime when only a run gives them. + OutputsKnown ReferenceScopeStepOutputsKnown `json:"outputsKnown"` + + // StepPath Step key such as steps[3] or handler_on.exit. + StepPath string `json:"stepPath"` + + // Upstream Names of all ancestor steps. + Upstream []string `json:"upstream"` +} + +// ReferenceScopeStepOutputsKnown static when the outputs are known from the spec, runtime when only a run gives them. +type ReferenceScopeStepOutputsKnown string + +// ReferenceScopeVariable A named value that a step can reference. +type ReferenceScopeVariable struct { + // DefaultValue Default value. Set only for params and consts. + DefaultValue *string `json:"defaultValue,omitempty"` + Description *string `json:"description,omitempty"` + Name string `json:"name"` + + // Reference Canonical reference token, such as ${params.region}. + Reference string `json:"reference"` +} + // RejectStepRequest Request body for rejecting a waiting step type RejectStepRequest struct { // Reason Optional reason for rejecting the step @@ -5387,6 +5628,39 @@ type ResourceHistory struct { MemoryUsedBytes *int64 `json:"memoryUsedBytes,omitempty"` } +// ReusedInput Provenance of the outputs that a skipped step of a selected-steps run carries. +type ReusedInput struct { + // CapturedAt RFC 3339 timestamp of when the outputs were captured. Absent when unknown. + CapturedAt *string `json:"capturedAt,omitempty"` + + // FinishedAt RFC 3339 timestamp with millisecond precision of when the step finished in the source run. Present for via run; absent for via cache. + FinishedAt *string `json:"finishedAt,omitempty"` + + // RunId Unique identifier for the DAG-run. The special value 'latest' can be used to reference the most recent DAG-run. + RunId DAGRunId `json:"runId"` + + // StartedAt RFC 3339 timestamp with millisecond precision of when the step started in the source run. Present for via run; absent for via cache. + StartedAt *string `json:"startedAt,omitempty"` + + // Step Name of the skipped step + Step string `json:"step"` + + // Via run: the outputs were carried from the run named by outputsFromRunId, or by outputsFromRuns for this step. cache: a client that is not the web UI supplied the outputs and named the run they were captured from in outputsSources. + Via ReusedInputVia `json:"via"` +} + +// ReusedInputVia run: the outputs were carried from the run named by outputsFromRunId, or by outputsFromRuns for this step. cache: a client that is not the web UI supplied the outputs and named the run they were captured from in outputsSources. +type ReusedInputVia string + +// ReusedOutputSource The run that a step's supplied outputs were captured from. +type ReusedOutputSource struct { + // CapturedAt RFC 3339 timestamp of when the outputs were captured. Optional. + CapturedAt *string `json:"capturedAt,omitempty"` + + // RunId Unique identifier for the DAG-run. The special value 'latest' can be used to reference the most recent DAG-run. + RunId DAGRunId `json:"runId"` +} + // RunConfig Configuration for controlling user interactions when starting DAG runs type RunConfig struct { // DisableParamEdit Disable parameter editing when starting the DAG @@ -5485,6 +5759,22 @@ type RuntimeProfileResponse struct { // RuntimeProfileStatus defines model for RuntimeProfileStatus. type RuntimeProfileStatus string +// SampleBucket The samples of one time bucket of a continuous step +type SampleBucket struct { + // Count Count of samples in the bucket + Count int64 `json:"count"` + + // Failures Count of failed samples (exit code not 0) + Failures int64 `json:"failures"` + MaxMs int64 `json:"maxMs"` + MinMs int64 `json:"minMs"` + P50Ms int64 `json:"p50Ms"` + P95Ms int64 `json:"p95Ms"` + + // Time Start of the bucket, in UTC + Time time.Time `json:"time"` +} + // Schedule Schedule configuration for DAG-run creation type Schedule struct { // At RFC 3339 timestamp with explicit offset for one-off schedules @@ -5758,6 +6048,46 @@ type Step_Parallel_Items struct { union json.RawMessage } +// StepAvailability defines model for StepAvailability. +type StepAvailability struct { + // LastFailureAt Time of the last failed sample, or the start of its aggregate bucket once the raw tier no longer holds it. Absent when none failed. + LastFailureAt *time.Time `json:"lastFailureAt,omitempty"` + + // Streak Count of samples since the last failed sample. A lower bound once the raw tier no longer holds that sample. + Streak int64 `json:"streak"` + + // TotalCount Count of all executions + TotalCount int64 `json:"totalCount"` + Windows []AvailabilityWindow `json:"windows"` +} + +// StepEvent defines model for StepEvent. +type StepEvent struct { + ExitCode *int `json:"exitCode,omitempty"` + + // FailedCount For `recovered` and for a repeated `failed` event: count of failed samples of the outage + FailedCount *int64 `json:"failedCount,omitempty"` + Kind StepEventKind `json:"kind"` + + // Message Last line of the standard error of a failed execution, 256 bytes at most + Message *string `json:"message,omitempty"` + + // OutageSeconds For `recovered`: length of the outage + OutageSeconds *int64 `json:"outageSeconds,omitempty"` + Time time.Time `json:"time"` +} + +// StepEventKind defines model for StepEventKind. +type StepEventKind string + +// StepEvents defines model for StepEvents. +type StepEvents struct { + Events []StepEvent `json:"events"` + + // NextCursor Cursor for the next page. Absent when there are no more events. + NextCursor *string `json:"nextCursor,omitempty"` +} + // StepInputDeclaration One named regular-file input for build execution type StepInputDeclaration struct { Name string `json:"name"` @@ -5788,6 +6118,54 @@ type StepOutputDeclaration struct { // StepOutputDeclarationType Output value type. JSON outputs must contain valid JSON text. type StepOutputDeclarationType string +// StepOutputValue One output value of a step. The value is cut at 65536 bytes. +type StepOutputValue struct { + Name string `json:"name"` + + // Size Size of the value in bytes before the cut. + Size int `json:"size"` + + // Truncated True when the value was cut. + Truncated bool `json:"truncated"` + Value string `json:"value"` +} + +// StepOutputs Persisted outputs of one step in a DAG-run. +type StepOutputs struct { + // OutputVariable One output value of a step. The value is cut at 65536 bytes. + OutputVariable *StepOutputValue `json:"outputVariable,omitempty"` + Outputs []StepOutputValue `json:"outputs"` + + // Status Numeric status code indicating current node state: + // 0: "Not started" + // 1: "Running" + // 2: "Failed" + // 3: "Aborted" + // 4: "Success" + // 5: "Skipped" + // 6: "Partial Success" + // 7: "Waiting for manual action" + // 8: "Rejected" + // 9: "Retrying" + Status NodeStatus `json:"status"` + + // StatusLabel Human-readable status description for the node + StatusLabel NodeStatusLabel `json:"statusLabel"` + StepId *string `json:"stepId,omitempty"` + StepName string `json:"stepName"` +} + +// StepSamples defines model for StepSamples. +type StepSamples struct { + // Buckets Buckets in time order. A bucket with no sample is not listed. + Buckets []SampleBucket `json:"buckets"` + From time.Time `json:"from"` + + // ResolutionSeconds Width of the tier that the buckets come from: 1, 60, 3600 or 86400 + ResolutionSeconds int `json:"resolutionSeconds"` + To time.Time `json:"to"` +} + // Stream defines model for Stream. type Stream string @@ -5811,13 +6189,13 @@ type SubDAGRunDetail struct { // DagRunId Unique identifier for the DAG-run. The special value 'latest' can be used to reference the most recent DAG-run. DagRunId DAGRunId `json:"dagRunId"` - // FinishedAt RFC 3339 timestamp when the sub DAG-run finished + // FinishedAt RFC 3339 timestamp with millisecond precision when the sub DAG-run finished FinishedAt *string `json:"finishedAt,omitempty"` // Params Parameters passed to the sub DAG-run in JSON format Params *string `json:"params,omitempty"` - // StartedAt RFC 3339 timestamp when the sub DAG-run started + // StartedAt RFC 3339 timestamp with millisecond precision when the sub DAG-run started StartedAt string `json:"startedAt"` // Status Numeric status code indicating current DAG-run state: @@ -6406,6 +6784,42 @@ type UsersListResponse struct { Users []User `json:"users"` } +// ValidationIssue One structured validation result of a DAG spec. +type ValidationIssue struct { + // Column 1-based column in the spec, when known. + Column *int `json:"column,omitempty"` + + // FieldPath DAG field path such as steps[3].with.url or consts.image, when known. + FieldPath *string `json:"fieldPath,omitempty"` + + // Line 1-based line in the spec, when known. + Line *int `json:"line,omitempty"` + + // Message Human-readable message. For errors, the same text as the matching errors entry. + Message string `json:"message"` + + // Reason Machine-readable reason, for value-reference notices. + Reason *string `json:"reason,omitempty"` + + // Severity Severity of the issue. + Severity ValidationIssueSeverity `json:"severity"` + + // Source Stage that produced the issue. + Source ValidationIssueSource `json:"source"` + + // StepName Normalized name of the step, when known. + StepName *string `json:"stepName,omitempty"` + + // StepPath Step key such as steps[3] or handler_on.failure, when known. + StepPath *string `json:"stepPath,omitempty"` +} + +// ValidationIssueSeverity Severity of the issue. +type ValidationIssueSeverity string + +// ValidationIssueSource Stage that produced the issue. +type ValidationIssueSource string + // ValueReferenceNotice A passive notice for a supported value reference left unresolved while loading a spec. type ValueReferenceNotice struct { // Class Whether the reference is a defect in the spec or its availability depends on runtime values or lifecycle scope. @@ -7457,6 +7871,9 @@ type ListDAGRunsParams struct { type ExecuteDAGRunFromSpecJSONBody struct { DagRunId *string `json:"dagRunId,omitempty"` + // Ephemeral If true, the run leaves no trace. It runs like any run and is readable by its ID (details, step logs, step outputs, spec, SSE) while it exists, but it is not listed anywhere by default (run lists, DAG history unless includeEphemeral is set, last run, queues, dashboard, artifacts, events and notifications), it does not count in retention, and it cannot be retried, rescheduled or edited (409). The server removes it 10 minutes after it ends and at server start. Allowed only together with steps; a request for the whole DAG with ephemeral true returns 400. The audit log keeps the request, marked ephemeral. + Ephemeral *bool `json:"ephemeral,omitempty"` + // Labels Additional labels to apply to the DAG-run. Mutually exclusive with `tags`; the server returns HTTP 400 if both are set. Labels *Labels `json:"labels,omitempty"` @@ -7466,10 +7883,16 @@ type ExecuteDAGRunFromSpecJSONBody struct { // NoReuse If true, execute eligible build steps without reusing prior materializations NoReuse *bool `json:"noReuse,omitempty"` - // Outputs Outputs of skipped steps, keyed by step name or ID and then by output name. A declared output, or any name when the step declares none, resolves through ${steps..outputs.}; the name of the step's string-form output variable sets that variable. Takes precedence over outputs from outputsFromRunId. Requires steps. + // Outputs Outputs of skipped steps, keyed by step name or ID and then by output name. For clients that are not the web UI; the web UI sends no values. A declared output, or any name when the step declares none, resolves through ${steps..outputs.}; the name of the step's string-form output variable sets that variable. Takes precedence over outputs from outputsFromRunId and outputsFromRuns, and works without them: the caller can supply every value itself, and can name the run the values of each step come from in outputsSources. A reference to an output that nobody supplied stays unresolved text in the step. Requires steps. Outputs *map[string]map[string]string `json:"outputs,omitempty"` OutputsFromRunId *string `json:"outputsFromRunId,omitempty"` + // OutputsFromRuns The run that each skipped step carries its outputs from, keyed by step name or ID. A step named here takes its outputs from that run in place of outputsFromRunId; the work directory still comes only from outputsFromRunId. Each run must exist for the same DAG (a normal or an ephemeral run), be finished, and hold a node of the step that succeeded or that carries copied outputs; otherwise the request returns 400 with a message that names the step and the run. A step selected to run cannot be a key. The web UI names the run of each reused step with this field and never sends values. Requires steps. + OutputsFromRuns *map[string]DAGRunId `json:"outputsFromRuns,omitempty"` + + // OutputsSources Optional. The run each step of outputs was captured from, keyed by step name or ID. Dagu records it in the reusedInputs of the new run; a step of outputs without an entry has no entry there. An entry for a step not in outputs returns 400. The run is not required to exist any more. Requires steps. + OutputsSources *map[string]ReusedOutputSource `json:"outputsSources,omitempty"` + // Params Parameters to pass to the DAG-run in JSON format Params *string `json:"params,omitempty"` @@ -7778,6 +8201,30 @@ type ApproveDAGRunStepParams struct { RemoteNode *RemoteNode `form:"remoteNode,omitempty" json:"remoteNode,omitempty"` } +// GetDAGRunStepAvailabilityParams defines parameters for GetDAGRunStepAvailability. +type GetDAGRunStepAvailabilityParams struct { + // RemoteNode name of the remote node + RemoteNode *RemoteNode `form:"remoteNode,omitempty" json:"remoteNode,omitempty"` +} + +// GetDAGRunStepEventsParams defines parameters for GetDAGRunStepEvents. +type GetDAGRunStepEventsParams struct { + // RemoteNode name of the remote node + RemoteNode *RemoteNode `form:"remoteNode,omitempty" json:"remoteNode,omitempty"` + + // From Start of the window (RFC 3339), inclusive. Default: no bound. + From *time.Time `form:"from,omitempty" json:"from,omitempty"` + + // To End of the window (RFC 3339), exclusive. Default: no bound. + To *time.Time `form:"to,omitempty" json:"to,omitempty"` + + // Limit Most events to return. + Limit *int `form:"limit,omitempty" json:"limit,omitempty"` + + // Before Opaque cursor from `nextCursor` of a previous response. + Before *string `form:"before,omitempty" json:"before,omitempty"` +} + // GetDAGRunForeachItemsParams defines parameters for GetDAGRunForeachItems. type GetDAGRunForeachItemsParams struct { // RemoteNode name of the remote node @@ -7869,6 +8316,12 @@ type GetDAGRunStepMessagesParams struct { RemoteNode *RemoteNode `form:"remoteNode,omitempty" json:"remoteNode,omitempty"` } +// GetDAGRunStepOutputsParams defines parameters for GetDAGRunStepOutputs. +type GetDAGRunStepOutputsParams struct { + // RemoteNode name of the remote node + RemoteNode *RemoteNode `form:"remoteNode,omitempty" json:"remoteNode,omitempty"` +} + // PushBackDAGRunStepParams defines parameters for PushBackDAGRunStep. type PushBackDAGRunStepParams struct { // RemoteNode name of the remote node @@ -7881,6 +8334,21 @@ type RejectDAGRunStepParams struct { RemoteNode *RemoteNode `form:"remoteNode,omitempty" json:"remoteNode,omitempty"` } +// GetDAGRunStepSamplesParams defines parameters for GetDAGRunStepSamples. +type GetDAGRunStepSamplesParams struct { + // RemoteNode name of the remote node + RemoteNode *RemoteNode `form:"remoteNode,omitempty" json:"remoteNode,omitempty"` + + // From Start of the window (RFC 3339). Default: 24 hours before `to`. + From *time.Time `form:"from,omitempty" json:"from,omitempty"` + + // To End of the window (RFC 3339). Default: now. + To *time.Time `form:"to,omitempty" json:"to,omitempty"` + + // Points Most buckets to return. + Points *int `form:"points,omitempty" json:"points,omitempty"` +} + // UpdateDAGRunStepStatusJSONBody defines parameters for UpdateDAGRunStepStatus. type UpdateDAGRunStepStatusJSONBody struct { // Status Numeric status code indicating current node state: @@ -8267,6 +8735,9 @@ type ClearDAGBrowserCacheParams struct { type GetDAGDAGRunHistoryParams struct { // RemoteNode name of the remote node RemoteNode *RemoteNode `form:"remoteNode,omitempty" json:"remoteNode,omitempty"` + + // IncludeEphemeral If true, the response also contains the ephemeral runs of this DAG, merged by start time with the other runs under the same limit. Each carries ephemeral true. No other list returns them. + IncludeEphemeral *bool `form:"includeEphemeral,omitempty" json:"includeEphemeral,omitempty"` } // GetDAGDAGRunDetailsParams defines parameters for GetDAGDAGRunDetails. @@ -8403,6 +8874,9 @@ type GetDAGSpecParams struct { // UpdateDAGSpecJSONBody defines parameters for UpdateDAGSpec. type UpdateDAGSpecJSONBody struct { + // ExpectedSha256 Lowercase hex SHA-256 of the file that the client edited. When set and different from the current file, the server writes nothing and returns 409. + ExpectedSha256 *string `json:"expectedSha256,omitempty"` + // Spec The new DAG spec in YAML format Spec string `json:"spec"` } @@ -8419,16 +8893,25 @@ type ExecuteDAGJSONBody struct { DagName *string `json:"dagName,omitempty"` DagRunId *string `json:"dagRunId,omitempty"` + // Ephemeral If true, the run leaves no trace. It runs like any run and is readable by its ID (details, step logs, step outputs, spec, SSE) while it exists, but it is not listed anywhere by default (run lists, DAG history unless includeEphemeral is set, last run, queues, dashboard, artifacts, events and notifications), it does not count in retention, and it cannot be retried, rescheduled or edited (409). The server removes it 10 minutes after it ends and at server start. Allowed only together with steps; a request for the whole DAG with ephemeral true returns 400. The audit log keeps the request, marked ephemeral. + Ephemeral *bool `json:"ephemeral,omitempty"` + // Labels Additional labels to apply to the DAG-run. Mutually exclusive with `tags`; the server returns HTTP 400 if both are set. Labels *Labels `json:"labels,omitempty"` // NoReuse If true, execute eligible build steps without reusing prior materializations NoReuse *bool `json:"noReuse,omitempty"` - // Outputs Outputs of skipped steps, keyed by step name or ID and then by output name. A declared output, or any name when the step declares none, resolves through ${steps..outputs.}; the name of the step's string-form output variable sets that variable. Takes precedence over outputs from outputsFromRunId. Requires steps. + // Outputs Outputs of skipped steps, keyed by step name or ID and then by output name. For clients that are not the web UI; the web UI sends no values. A declared output, or any name when the step declares none, resolves through ${steps..outputs.}; the name of the step's string-form output variable sets that variable. Takes precedence over outputs from outputsFromRunId and outputsFromRuns, and works without them: the caller can supply every value itself, and can name the run the values of each step come from in outputsSources. A reference to an output that nobody supplied stays unresolved text in the step. Requires steps. Outputs *map[string]map[string]string `json:"outputs,omitempty"` OutputsFromRunId *string `json:"outputsFromRunId,omitempty"` + // OutputsFromRuns The run that each skipped step carries its outputs from, keyed by step name or ID. A step named here takes its outputs from that run in place of outputsFromRunId; the work directory still comes only from outputsFromRunId. Each run must exist for the same DAG (a normal or an ephemeral run), be finished, and hold a node of the step that succeeded or that carries copied outputs; otherwise the request returns 400 with a message that names the step and the run. A step selected to run cannot be a key. The web UI names the run of each reused step with this field and never sends values. Requires steps. + OutputsFromRuns *map[string]DAGRunId `json:"outputsFromRuns,omitempty"` + + // OutputsSources Optional. The run each step of outputs was captured from, keyed by step name or ID. Dagu records it in the reusedInputs of the new run; a step of outputs without an entry has no entry there. An entry for a step not in outputs returns 400. The run is not required to exist any more. Requires steps. + OutputsSources *map[string]ReusedOutputSource `json:"outputsSources,omitempty"` + // Params Parameters to pass to the DAG-run in JSON format Params *string `json:"params,omitempty"` @@ -10447,6 +10930,12 @@ type ServerInterface interface { // Approve a waiting step // (POST /dag-runs/{name}/{dagRunId}/steps/{stepName}/approve) ApproveDAGRunStep(w http.ResponseWriter, r *http.Request, name DAGName, dagRunId DAGRunId, stepName StepName, params ApproveDAGRunStepParams) + // Retrieve the availability of a continuous step + // (GET /dag-runs/{name}/{dagRunId}/steps/{stepName}/availability) + GetDAGRunStepAvailability(w http.ResponseWriter, r *http.Request, name DAGName, dagRunId DAGRunId, stepName StepName, params GetDAGRunStepAvailabilityParams) + // Retrieve the events of a continuous step + // (GET /dag-runs/{name}/{dagRunId}/steps/{stepName}/events) + GetDAGRunStepEvents(w http.ResponseWriter, r *http.Request, name DAGName, dagRunId DAGRunId, stepName StepName, params GetDAGRunStepEventsParams) // List the items of a foreach step in a DAG-run // (GET /dag-runs/{name}/{dagRunId}/steps/{stepName}/foreach) GetDAGRunForeachItems(w http.ResponseWriter, r *http.Request, name DAGName, dagRunId DAGRunId, stepName StepName, params GetDAGRunForeachItemsParams) @@ -10468,12 +10957,18 @@ type ServerInterface interface { // Retrieve chat messages for a step // (GET /dag-runs/{name}/{dagRunId}/steps/{stepName}/messages) GetDAGRunStepMessages(w http.ResponseWriter, r *http.Request, name DAGName, dagRunId DAGRunId, stepName StepName, params GetDAGRunStepMessagesParams) + // Retrieve outputs of a specific step in a DAG-run + // (GET /dag-runs/{name}/{dagRunId}/steps/{stepName}/outputs) + GetDAGRunStepOutputs(w http.ResponseWriter, r *http.Request, name DAGName, dagRunId DAGRunId, stepName StepName, params GetDAGRunStepOutputsParams) // Push back a waiting step for re-execution with feedback // (POST /dag-runs/{name}/{dagRunId}/steps/{stepName}/push-back) PushBackDAGRunStep(w http.ResponseWriter, r *http.Request, name DAGName, dagRunId DAGRunId, stepName StepName, params PushBackDAGRunStepParams) // Reject a waiting step // (POST /dag-runs/{name}/{dagRunId}/steps/{stepName}/reject) RejectDAGRunStep(w http.ResponseWriter, r *http.Request, name DAGName, dagRunId DAGRunId, stepName StepName, params RejectDAGRunStepParams) + // Retrieve the samples of a continuous step, in buckets + // (GET /dag-runs/{name}/{dagRunId}/steps/{stepName}/samples) + GetDAGRunStepSamples(w http.ResponseWriter, r *http.Request, name DAGName, dagRunId DAGRunId, stepName StepName, params GetDAGRunStepSamplesParams) // Manually update a step's execution status // (PATCH /dag-runs/{name}/{dagRunId}/steps/{stepName}/status) UpdateDAGRunStepStatus(w http.ResponseWriter, r *http.Request, name DAGName, dagRunId DAGRunId, stepName StepName, params UpdateDAGRunStepStatusParams) @@ -11353,6 +11848,18 @@ func (_ Unimplemented) ApproveDAGRunStep(w http.ResponseWriter, r *http.Request, w.WriteHeader(http.StatusNotImplemented) } +// Retrieve the availability of a continuous step +// (GET /dag-runs/{name}/{dagRunId}/steps/{stepName}/availability) +func (_ Unimplemented) GetDAGRunStepAvailability(w http.ResponseWriter, r *http.Request, name DAGName, dagRunId DAGRunId, stepName StepName, params GetDAGRunStepAvailabilityParams) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Retrieve the events of a continuous step +// (GET /dag-runs/{name}/{dagRunId}/steps/{stepName}/events) +func (_ Unimplemented) GetDAGRunStepEvents(w http.ResponseWriter, r *http.Request, name DAGName, dagRunId DAGRunId, stepName StepName, params GetDAGRunStepEventsParams) { + w.WriteHeader(http.StatusNotImplemented) +} + // List the items of a foreach step in a DAG-run // (GET /dag-runs/{name}/{dagRunId}/steps/{stepName}/foreach) func (_ Unimplemented) GetDAGRunForeachItems(w http.ResponseWriter, r *http.Request, name DAGName, dagRunId DAGRunId, stepName StepName, params GetDAGRunForeachItemsParams) { @@ -11395,6 +11902,12 @@ func (_ Unimplemented) GetDAGRunStepMessages(w http.ResponseWriter, r *http.Requ w.WriteHeader(http.StatusNotImplemented) } +// Retrieve outputs of a specific step in a DAG-run +// (GET /dag-runs/{name}/{dagRunId}/steps/{stepName}/outputs) +func (_ Unimplemented) GetDAGRunStepOutputs(w http.ResponseWriter, r *http.Request, name DAGName, dagRunId DAGRunId, stepName StepName, params GetDAGRunStepOutputsParams) { + w.WriteHeader(http.StatusNotImplemented) +} + // Push back a waiting step for re-execution with feedback // (POST /dag-runs/{name}/{dagRunId}/steps/{stepName}/push-back) func (_ Unimplemented) PushBackDAGRunStep(w http.ResponseWriter, r *http.Request, name DAGName, dagRunId DAGRunId, stepName StepName, params PushBackDAGRunStepParams) { @@ -11407,6 +11920,12 @@ func (_ Unimplemented) RejectDAGRunStep(w http.ResponseWriter, r *http.Request, w.WriteHeader(http.StatusNotImplemented) } +// Retrieve the samples of a continuous step, in buckets +// (GET /dag-runs/{name}/{dagRunId}/steps/{stepName}/samples) +func (_ Unimplemented) GetDAGRunStepSamples(w http.ResponseWriter, r *http.Request, name DAGName, dagRunId DAGRunId, stepName StepName, params GetDAGRunStepSamplesParams) { + w.WriteHeader(http.StatusNotImplemented) +} + // Manually update a step's execution status // (PATCH /dag-runs/{name}/{dagRunId}/steps/{stepName}/status) func (_ Unimplemented) UpdateDAGRunStepStatus(w http.ResponseWriter, r *http.Request, name DAGName, dagRunId DAGRunId, stepName StepName, params UpdateDAGRunStepStatusParams) { @@ -15366,8 +15885,8 @@ func (siw *ServerInterfaceWrapper) ApproveDAGRunStep(w http.ResponseWriter, r *h handler.ServeHTTP(w, r) } -// GetDAGRunForeachItems operation middleware -func (siw *ServerInterfaceWrapper) GetDAGRunForeachItems(w http.ResponseWriter, r *http.Request) { +// GetDAGRunStepAvailability operation middleware +func (siw *ServerInterfaceWrapper) GetDAGRunStepAvailability(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -15408,7 +15927,7 @@ func (siw *ServerInterfaceWrapper) GetDAGRunForeachItems(w http.ResponseWriter, r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params GetDAGRunForeachItemsParams + var params GetDAGRunStepAvailabilityParams // ------------- Optional query parameter "remoteNode" ------------- @@ -15423,60 +15942,8 @@ func (siw *ServerInterfaceWrapper) GetDAGRunForeachItems(w http.ResponseWriter, return } - // ------------- Optional query parameter "parent" ------------- - - err = runtime.BindQueryParameterWithOptions("form", true, false, "parent", r.URL.Query(), ¶ms.Parent, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) - if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "parent"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "parent", Err: err}) - } - return - } - - // ------------- Optional query parameter "status" ------------- - - err = runtime.BindQueryParameterWithOptions("form", true, false, "status", r.URL.Query(), ¶ms.Status, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) - if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "status"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "status", Err: err}) - } - return - } - - // ------------- Optional query parameter "page" ------------- - - err = runtime.BindQueryParameterWithOptions("form", true, false, "page", r.URL.Query(), ¶ms.Page, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) - if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "page"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "page", Err: err}) - } - return - } - - // ------------- Optional query parameter "perPage" ------------- - - err = runtime.BindQueryParameterWithOptions("form", true, false, "perPage", r.URL.Query(), ¶ms.PerPage, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) - if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "perPage"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "perPage", Err: err}) - } - return - } - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetDAGRunForeachItems(w, r, name, dagRunId, stepName, params) + siw.Handler.GetDAGRunStepAvailability(w, r, name, dagRunId, stepName, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -15486,8 +15953,8 @@ func (siw *ServerInterfaceWrapper) GetDAGRunForeachItems(w http.ResponseWriter, handler.ServeHTTP(w, r) } -// GetDAGRunForeachItem operation middleware -func (siw *ServerInterfaceWrapper) GetDAGRunForeachItem(w http.ResponseWriter, r *http.Request) { +// GetDAGRunStepEvents operation middleware +func (siw *ServerInterfaceWrapper) GetDAGRunStepEvents(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -15519,15 +15986,6 @@ func (siw *ServerInterfaceWrapper) GetDAGRunForeachItem(w http.ResponseWriter, r return } - // ------------- Path parameter "item" ------------- - var item ForeachItemPath - - err = runtime.BindStyledParameterWithOptions("simple", "item", chi.URLParam(r, "item"), &item, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "item", Err: err}) - return - } - ctx := r.Context() ctx = context.WithValue(ctx, ApiTokenScopes, []string{}) @@ -15537,7 +15995,7 @@ func (siw *ServerInterfaceWrapper) GetDAGRunForeachItem(w http.ResponseWriter, r r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params GetDAGRunForeachItemParams + var params GetDAGRunStepEventsParams // ------------- Optional query parameter "remoteNode" ------------- @@ -15552,8 +16010,60 @@ func (siw *ServerInterfaceWrapper) GetDAGRunForeachItem(w http.ResponseWriter, r return } + // ------------- Optional query parameter "from" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "from", r.URL.Query(), ¶ms.From, runtime.BindQueryParameterOptions{Type: "string", Format: "date-time"}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "from"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "from", Err: err}) + } + return + } + + // ------------- Optional query parameter "to" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "to", r.URL.Query(), ¶ms.To, runtime.BindQueryParameterOptions{Type: "string", Format: "date-time"}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "to"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "to", Err: err}) + } + return + } + + // ------------- Optional query parameter "limit" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "limit", r.URL.Query(), ¶ms.Limit, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "limit"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err}) + } + return + } + + // ------------- Optional query parameter "before" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "before", r.URL.Query(), ¶ms.Before, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "before"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "before", Err: err}) + } + return + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetDAGRunForeachItem(w, r, name, dagRunId, stepName, item, params) + siw.Handler.GetDAGRunStepEvents(w, r, name, dagRunId, stepName, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -15563,8 +16073,8 @@ func (siw *ServerInterfaceWrapper) GetDAGRunForeachItem(w http.ResponseWriter, r handler.ServeHTTP(w, r) } -// GetDAGRunForeachStepLog operation middleware -func (siw *ServerInterfaceWrapper) GetDAGRunForeachStepLog(w http.ResponseWriter, r *http.Request) { +// GetDAGRunForeachItems operation middleware +func (siw *ServerInterfaceWrapper) GetDAGRunForeachItems(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -15596,24 +16106,6 @@ func (siw *ServerInterfaceWrapper) GetDAGRunForeachStepLog(w http.ResponseWriter return } - // ------------- Path parameter "item" ------------- - var item ForeachItemPath - - err = runtime.BindStyledParameterWithOptions("simple", "item", chi.URLParam(r, "item"), &item, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "item", Err: err}) - return - } - - // ------------- Path parameter "bodyStepName" ------------- - var bodyStepName BodyStepName - - err = runtime.BindStyledParameterWithOptions("simple", "bodyStepName", chi.URLParam(r, "bodyStepName"), &bodyStepName, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "bodyStepName", Err: err}) - return - } - ctx := r.Context() ctx = context.WithValue(ctx, ApiTokenScopes, []string{}) @@ -15623,7 +16115,7 @@ func (siw *ServerInterfaceWrapper) GetDAGRunForeachStepLog(w http.ResponseWriter r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params GetDAGRunForeachStepLogParams + var params GetDAGRunForeachItemsParams // ------------- Optional query parameter "remoteNode" ------------- @@ -15638,73 +16130,60 @@ func (siw *ServerInterfaceWrapper) GetDAGRunForeachStepLog(w http.ResponseWriter return } - // ------------- Optional query parameter "tail" ------------- - - err = runtime.BindQueryParameterWithOptions("form", true, false, "tail", r.URL.Query(), ¶ms.Tail, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) - if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "tail"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "tail", Err: err}) - } - return - } - - // ------------- Optional query parameter "head" ------------- + // ------------- Optional query parameter "parent" ------------- - err = runtime.BindQueryParameterWithOptions("form", true, false, "head", r.URL.Query(), ¶ms.Head, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + err = runtime.BindQueryParameterWithOptions("form", true, false, "parent", r.URL.Query(), ¶ms.Parent, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) if err != nil { var requiredError *runtime.RequiredParameterError if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "head"}) + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "parent"}) } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "head", Err: err}) + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "parent", Err: err}) } return } - // ------------- Optional query parameter "offset" ------------- + // ------------- Optional query parameter "status" ------------- - err = runtime.BindQueryParameterWithOptions("form", true, false, "offset", r.URL.Query(), ¶ms.Offset, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + err = runtime.BindQueryParameterWithOptions("form", true, false, "status", r.URL.Query(), ¶ms.Status, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) if err != nil { var requiredError *runtime.RequiredParameterError if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "offset"}) + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "status"}) } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "offset", Err: err}) + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "status", Err: err}) } return } - // ------------- Optional query parameter "limit" ------------- + // ------------- Optional query parameter "page" ------------- - err = runtime.BindQueryParameterWithOptions("form", true, false, "limit", r.URL.Query(), ¶ms.Limit, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + err = runtime.BindQueryParameterWithOptions("form", true, false, "page", r.URL.Query(), ¶ms.Page, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) if err != nil { var requiredError *runtime.RequiredParameterError if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "limit"}) + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "page"}) } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err}) + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "page", Err: err}) } return } - // ------------- Optional query parameter "stream" ------------- + // ------------- Optional query parameter "perPage" ------------- - err = runtime.BindQueryParameterWithOptions("form", true, false, "stream", r.URL.Query(), ¶ms.Stream, runtime.BindQueryParameterOptions{Type: "string", Format: "string"}) + err = runtime.BindQueryParameterWithOptions("form", true, false, "perPage", r.URL.Query(), ¶ms.PerPage, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) if err != nil { var requiredError *runtime.RequiredParameterError if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "stream"}) + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "perPage"}) } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stream", Err: err}) + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "perPage", Err: err}) } return } handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetDAGRunForeachStepLog(w, r, name, dagRunId, stepName, item, bodyStepName, params) + siw.Handler.GetDAGRunForeachItems(w, r, name, dagRunId, stepName, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -15714,8 +16193,8 @@ func (siw *ServerInterfaceWrapper) GetDAGRunForeachStepLog(w http.ResponseWriter handler.ServeHTTP(w, r) } -// DownloadDAGRunForeachStepLog operation middleware -func (siw *ServerInterfaceWrapper) DownloadDAGRunForeachStepLog(w http.ResponseWriter, r *http.Request) { +// GetDAGRunForeachItem operation middleware +func (siw *ServerInterfaceWrapper) GetDAGRunForeachItem(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -15756,15 +16235,6 @@ func (siw *ServerInterfaceWrapper) DownloadDAGRunForeachStepLog(w http.ResponseW return } - // ------------- Path parameter "bodyStepName" ------------- - var bodyStepName BodyStepName - - err = runtime.BindStyledParameterWithOptions("simple", "bodyStepName", chi.URLParam(r, "bodyStepName"), &bodyStepName, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "bodyStepName", Err: err}) - return - } - ctx := r.Context() ctx = context.WithValue(ctx, ApiTokenScopes, []string{}) @@ -15774,7 +16244,7 @@ func (siw *ServerInterfaceWrapper) DownloadDAGRunForeachStepLog(w http.ResponseW r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params DownloadDAGRunForeachStepLogParams + var params GetDAGRunForeachItemParams // ------------- Optional query parameter "remoteNode" ------------- @@ -15789,21 +16259,8 @@ func (siw *ServerInterfaceWrapper) DownloadDAGRunForeachStepLog(w http.ResponseW return } - // ------------- Optional query parameter "stream" ------------- - - err = runtime.BindQueryParameterWithOptions("form", true, false, "stream", r.URL.Query(), ¶ms.Stream, runtime.BindQueryParameterOptions{Type: "string", Format: "string"}) - if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "stream"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stream", Err: err}) - } - return - } - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.DownloadDAGRunForeachStepLog(w, r, name, dagRunId, stepName, item, bodyStepName, params) + siw.Handler.GetDAGRunForeachItem(w, r, name, dagRunId, stepName, item, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -15813,8 +16270,8 @@ func (siw *ServerInterfaceWrapper) DownloadDAGRunForeachStepLog(w http.ResponseW handler.ServeHTTP(w, r) } -// GetDAGRunStepLog operation middleware -func (siw *ServerInterfaceWrapper) GetDAGRunStepLog(w http.ResponseWriter, r *http.Request) { +// GetDAGRunForeachStepLog operation middleware +func (siw *ServerInterfaceWrapper) GetDAGRunForeachStepLog(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -15846,6 +16303,24 @@ func (siw *ServerInterfaceWrapper) GetDAGRunStepLog(w http.ResponseWriter, r *ht return } + // ------------- Path parameter "item" ------------- + var item ForeachItemPath + + err = runtime.BindStyledParameterWithOptions("simple", "item", chi.URLParam(r, "item"), &item, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "item", Err: err}) + return + } + + // ------------- Path parameter "bodyStepName" ------------- + var bodyStepName BodyStepName + + err = runtime.BindStyledParameterWithOptions("simple", "bodyStepName", chi.URLParam(r, "bodyStepName"), &bodyStepName, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "bodyStepName", Err: err}) + return + } + ctx := r.Context() ctx = context.WithValue(ctx, ApiTokenScopes, []string{}) @@ -15855,7 +16330,7 @@ func (siw *ServerInterfaceWrapper) GetDAGRunStepLog(w http.ResponseWriter, r *ht r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params GetDAGRunStepLogParams + var params GetDAGRunForeachStepLogParams // ------------- Optional query parameter "remoteNode" ------------- @@ -15936,7 +16411,7 @@ func (siw *ServerInterfaceWrapper) GetDAGRunStepLog(w http.ResponseWriter, r *ht } handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetDAGRunStepLog(w, r, name, dagRunId, stepName, params) + siw.Handler.GetDAGRunForeachStepLog(w, r, name, dagRunId, stepName, item, bodyStepName, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -15946,8 +16421,8 @@ func (siw *ServerInterfaceWrapper) GetDAGRunStepLog(w http.ResponseWriter, r *ht handler.ServeHTTP(w, r) } -// DownloadDAGRunStepLog operation middleware -func (siw *ServerInterfaceWrapper) DownloadDAGRunStepLog(w http.ResponseWriter, r *http.Request) { +// DownloadDAGRunForeachStepLog operation middleware +func (siw *ServerInterfaceWrapper) DownloadDAGRunForeachStepLog(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -15979,6 +16454,24 @@ func (siw *ServerInterfaceWrapper) DownloadDAGRunStepLog(w http.ResponseWriter, return } + // ------------- Path parameter "item" ------------- + var item ForeachItemPath + + err = runtime.BindStyledParameterWithOptions("simple", "item", chi.URLParam(r, "item"), &item, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "item", Err: err}) + return + } + + // ------------- Path parameter "bodyStepName" ------------- + var bodyStepName BodyStepName + + err = runtime.BindStyledParameterWithOptions("simple", "bodyStepName", chi.URLParam(r, "bodyStepName"), &bodyStepName, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "bodyStepName", Err: err}) + return + } + ctx := r.Context() ctx = context.WithValue(ctx, ApiTokenScopes, []string{}) @@ -15988,7 +16481,7 @@ func (siw *ServerInterfaceWrapper) DownloadDAGRunStepLog(w http.ResponseWriter, r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params DownloadDAGRunStepLogParams + var params DownloadDAGRunForeachStepLogParams // ------------- Optional query parameter "remoteNode" ------------- @@ -16017,7 +16510,7 @@ func (siw *ServerInterfaceWrapper) DownloadDAGRunStepLog(w http.ResponseWriter, } handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.DownloadDAGRunStepLog(w, r, name, dagRunId, stepName, params) + siw.Handler.DownloadDAGRunForeachStepLog(w, r, name, dagRunId, stepName, item, bodyStepName, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -16027,8 +16520,8 @@ func (siw *ServerInterfaceWrapper) DownloadDAGRunStepLog(w http.ResponseWriter, handler.ServeHTTP(w, r) } -// GetDAGRunStepMessages operation middleware -func (siw *ServerInterfaceWrapper) GetDAGRunStepMessages(w http.ResponseWriter, r *http.Request) { +// GetDAGRunStepLog operation middleware +func (siw *ServerInterfaceWrapper) GetDAGRunStepLog(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -16069,7 +16562,7 @@ func (siw *ServerInterfaceWrapper) GetDAGRunStepMessages(w http.ResponseWriter, r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params GetDAGRunStepMessagesParams + var params GetDAGRunStepLogParams // ------------- Optional query parameter "remoteNode" ------------- @@ -16084,8 +16577,73 @@ func (siw *ServerInterfaceWrapper) GetDAGRunStepMessages(w http.ResponseWriter, return } + // ------------- Optional query parameter "tail" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "tail", r.URL.Query(), ¶ms.Tail, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "tail"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "tail", Err: err}) + } + return + } + + // ------------- Optional query parameter "head" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "head", r.URL.Query(), ¶ms.Head, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "head"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "head", Err: err}) + } + return + } + + // ------------- Optional query parameter "offset" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "offset", r.URL.Query(), ¶ms.Offset, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "offset"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "offset", Err: err}) + } + return + } + + // ------------- Optional query parameter "limit" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "limit", r.URL.Query(), ¶ms.Limit, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "limit"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err}) + } + return + } + + // ------------- Optional query parameter "stream" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "stream", r.URL.Query(), ¶ms.Stream, runtime.BindQueryParameterOptions{Type: "string", Format: "string"}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "stream"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stream", Err: err}) + } + return + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetDAGRunStepMessages(w, r, name, dagRunId, stepName, params) + siw.Handler.GetDAGRunStepLog(w, r, name, dagRunId, stepName, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -16095,8 +16653,8 @@ func (siw *ServerInterfaceWrapper) GetDAGRunStepMessages(w http.ResponseWriter, handler.ServeHTTP(w, r) } -// PushBackDAGRunStep operation middleware -func (siw *ServerInterfaceWrapper) PushBackDAGRunStep(w http.ResponseWriter, r *http.Request) { +// DownloadDAGRunStepLog operation middleware +func (siw *ServerInterfaceWrapper) DownloadDAGRunStepLog(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -16137,7 +16695,7 @@ func (siw *ServerInterfaceWrapper) PushBackDAGRunStep(w http.ResponseWriter, r * r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params PushBackDAGRunStepParams + var params DownloadDAGRunStepLogParams // ------------- Optional query parameter "remoteNode" ------------- @@ -16152,8 +16710,21 @@ func (siw *ServerInterfaceWrapper) PushBackDAGRunStep(w http.ResponseWriter, r * return } + // ------------- Optional query parameter "stream" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "stream", r.URL.Query(), ¶ms.Stream, runtime.BindQueryParameterOptions{Type: "string", Format: "string"}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "stream"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stream", Err: err}) + } + return + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.PushBackDAGRunStep(w, r, name, dagRunId, stepName, params) + siw.Handler.DownloadDAGRunStepLog(w, r, name, dagRunId, stepName, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -16163,8 +16734,8 @@ func (siw *ServerInterfaceWrapper) PushBackDAGRunStep(w http.ResponseWriter, r * handler.ServeHTTP(w, r) } -// RejectDAGRunStep operation middleware -func (siw *ServerInterfaceWrapper) RejectDAGRunStep(w http.ResponseWriter, r *http.Request) { +// GetDAGRunStepMessages operation middleware +func (siw *ServerInterfaceWrapper) GetDAGRunStepMessages(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -16205,7 +16776,7 @@ func (siw *ServerInterfaceWrapper) RejectDAGRunStep(w http.ResponseWriter, r *ht r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params RejectDAGRunStepParams + var params GetDAGRunStepMessagesParams // ------------- Optional query parameter "remoteNode" ------------- @@ -16221,7 +16792,7 @@ func (siw *ServerInterfaceWrapper) RejectDAGRunStep(w http.ResponseWriter, r *ht } handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.RejectDAGRunStep(w, r, name, dagRunId, stepName, params) + siw.Handler.GetDAGRunStepMessages(w, r, name, dagRunId, stepName, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -16231,8 +16802,8 @@ func (siw *ServerInterfaceWrapper) RejectDAGRunStep(w http.ResponseWriter, r *ht handler.ServeHTTP(w, r) } -// UpdateDAGRunStepStatus operation middleware -func (siw *ServerInterfaceWrapper) UpdateDAGRunStepStatus(w http.ResponseWriter, r *http.Request) { +// GetDAGRunStepOutputs operation middleware +func (siw *ServerInterfaceWrapper) GetDAGRunStepOutputs(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -16273,7 +16844,7 @@ func (siw *ServerInterfaceWrapper) UpdateDAGRunStepStatus(w http.ResponseWriter, r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params UpdateDAGRunStepStatusParams + var params GetDAGRunStepOutputsParams // ------------- Optional query parameter "remoteNode" ------------- @@ -16289,7 +16860,7 @@ func (siw *ServerInterfaceWrapper) UpdateDAGRunStepStatus(w http.ResponseWriter, } handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.UpdateDAGRunStepStatus(w, r, name, dagRunId, stepName, params) + siw.Handler.GetDAGRunStepOutputs(w, r, name, dagRunId, stepName, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -16299,8 +16870,8 @@ func (siw *ServerInterfaceWrapper) UpdateDAGRunStepStatus(w http.ResponseWriter, handler.ServeHTTP(w, r) } -// TerminateDAGRun operation middleware -func (siw *ServerInterfaceWrapper) TerminateDAGRun(w http.ResponseWriter, r *http.Request) { +// PushBackDAGRunStep operation middleware +func (siw *ServerInterfaceWrapper) PushBackDAGRunStep(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -16323,6 +16894,15 @@ func (siw *ServerInterfaceWrapper) TerminateDAGRun(w http.ResponseWriter, r *htt return } + // ------------- Path parameter "stepName" ------------- + var stepName StepName + + err = runtime.BindStyledParameterWithOptions("simple", "stepName", chi.URLParam(r, "stepName"), &stepName, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stepName", Err: err}) + return + } + ctx := r.Context() ctx = context.WithValue(ctx, ApiTokenScopes, []string{}) @@ -16332,7 +16912,7 @@ func (siw *ServerInterfaceWrapper) TerminateDAGRun(w http.ResponseWriter, r *htt r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params TerminateDAGRunParams + var params PushBackDAGRunStepParams // ------------- Optional query parameter "remoteNode" ------------- @@ -16348,7 +16928,7 @@ func (siw *ServerInterfaceWrapper) TerminateDAGRun(w http.ResponseWriter, r *htt } handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.TerminateDAGRun(w, r, name, dagRunId, params) + siw.Handler.PushBackDAGRunStep(w, r, name, dagRunId, stepName, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -16358,8 +16938,8 @@ func (siw *ServerInterfaceWrapper) TerminateDAGRun(w http.ResponseWriter, r *htt handler.ServeHTTP(w, r) } -// GetSubDAGRuns operation middleware -func (siw *ServerInterfaceWrapper) GetSubDAGRuns(w http.ResponseWriter, r *http.Request) { +// RejectDAGRunStep operation middleware +func (siw *ServerInterfaceWrapper) RejectDAGRunStep(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -16382,6 +16962,15 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRuns(w http.ResponseWriter, r *http. return } + // ------------- Path parameter "stepName" ------------- + var stepName StepName + + err = runtime.BindStyledParameterWithOptions("simple", "stepName", chi.URLParam(r, "stepName"), &stepName, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stepName", Err: err}) + return + } + ctx := r.Context() ctx = context.WithValue(ctx, ApiTokenScopes, []string{}) @@ -16391,7 +16980,7 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRuns(w http.ResponseWriter, r *http. r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params GetSubDAGRunsParams + var params RejectDAGRunStepParams // ------------- Optional query parameter "remoteNode" ------------- @@ -16406,21 +16995,8 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRuns(w http.ResponseWriter, r *http. return } - // ------------- Optional query parameter "parentSubDAGRunId" ------------- - - err = runtime.BindQueryParameterWithOptions("form", true, false, "parentSubDAGRunId", r.URL.Query(), ¶ms.ParentSubDAGRunId, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) - if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "parentSubDAGRunId"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "parentSubDAGRunId", Err: err}) - } - return - } - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetSubDAGRuns(w, r, name, dagRunId, params) + siw.Handler.RejectDAGRunStep(w, r, name, dagRunId, stepName, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -16430,8 +17006,8 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRuns(w http.ResponseWriter, r *http. handler.ServeHTTP(w, r) } -// GetSubDAGRunDetails operation middleware -func (siw *ServerInterfaceWrapper) GetSubDAGRunDetails(w http.ResponseWriter, r *http.Request) { +// GetDAGRunStepSamples operation middleware +func (siw *ServerInterfaceWrapper) GetDAGRunStepSamples(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -16454,12 +17030,12 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunDetails(w http.ResponseWriter, r return } - // ------------- Path parameter "subDAGRunId" ------------- - var subDAGRunId string + // ------------- Path parameter "stepName" ------------- + var stepName StepName - err = runtime.BindStyledParameterWithOptions("simple", "subDAGRunId", chi.URLParam(r, "subDAGRunId"), &subDAGRunId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + err = runtime.BindStyledParameterWithOptions("simple", "stepName", chi.URLParam(r, "stepName"), &stepName, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "subDAGRunId", Err: err}) + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stepName", Err: err}) return } @@ -16472,7 +17048,7 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunDetails(w http.ResponseWriter, r r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params GetSubDAGRunDetailsParams + var params GetDAGRunStepSamplesParams // ------------- Optional query parameter "remoteNode" ------------- @@ -16487,8 +17063,47 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunDetails(w http.ResponseWriter, r return } + // ------------- Optional query parameter "from" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "from", r.URL.Query(), ¶ms.From, runtime.BindQueryParameterOptions{Type: "string", Format: "date-time"}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "from"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "from", Err: err}) + } + return + } + + // ------------- Optional query parameter "to" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "to", r.URL.Query(), ¶ms.To, runtime.BindQueryParameterOptions{Type: "string", Format: "date-time"}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "to"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "to", Err: err}) + } + return + } + + // ------------- Optional query parameter "points" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "points", r.URL.Query(), ¶ms.Points, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "points"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "points", Err: err}) + } + return + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetSubDAGRunDetails(w, r, name, dagRunId, subDAGRunId, params) + siw.Handler.GetDAGRunStepSamples(w, r, name, dagRunId, stepName, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -16498,8 +17113,8 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunDetails(w http.ResponseWriter, r handler.ServeHTTP(w, r) } -// GetSubDAGRunArtifacts operation middleware -func (siw *ServerInterfaceWrapper) GetSubDAGRunArtifacts(w http.ResponseWriter, r *http.Request) { +// UpdateDAGRunStepStatus operation middleware +func (siw *ServerInterfaceWrapper) UpdateDAGRunStepStatus(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -16522,12 +17137,12 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunArtifacts(w http.ResponseWriter, return } - // ------------- Path parameter "subDAGRunId" ------------- - var subDAGRunId string + // ------------- Path parameter "stepName" ------------- + var stepName StepName - err = runtime.BindStyledParameterWithOptions("simple", "subDAGRunId", chi.URLParam(r, "subDAGRunId"), &subDAGRunId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + err = runtime.BindStyledParameterWithOptions("simple", "stepName", chi.URLParam(r, "stepName"), &stepName, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "subDAGRunId", Err: err}) + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stepName", Err: err}) return } @@ -16540,7 +17155,7 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunArtifacts(w http.ResponseWriter, r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params GetSubDAGRunArtifactsParams + var params UpdateDAGRunStepStatusParams // ------------- Optional query parameter "remoteNode" ------------- @@ -16555,21 +17170,8 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunArtifacts(w http.ResponseWriter, return } - // ------------- Optional query parameter "recursive" ------------- - - err = runtime.BindQueryParameterWithOptions("form", true, false, "recursive", r.URL.Query(), ¶ms.Recursive, runtime.BindQueryParameterOptions{Type: "boolean", Format: ""}) - if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "recursive"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "recursive", Err: err}) - } - return - } - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetSubDAGRunArtifacts(w, r, name, dagRunId, subDAGRunId, params) + siw.Handler.UpdateDAGRunStepStatus(w, r, name, dagRunId, stepName, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -16579,8 +17181,8 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunArtifacts(w http.ResponseWriter, handler.ServeHTTP(w, r) } -// DownloadSubDAGRunArtifact operation middleware -func (siw *ServerInterfaceWrapper) DownloadSubDAGRunArtifact(w http.ResponseWriter, r *http.Request) { +// TerminateDAGRun operation middleware +func (siw *ServerInterfaceWrapper) TerminateDAGRun(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -16603,15 +17205,6 @@ func (siw *ServerInterfaceWrapper) DownloadSubDAGRunArtifact(w http.ResponseWrit return } - // ------------- Path parameter "subDAGRunId" ------------- - var subDAGRunId string - - err = runtime.BindStyledParameterWithOptions("simple", "subDAGRunId", chi.URLParam(r, "subDAGRunId"), &subDAGRunId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "subDAGRunId", Err: err}) - return - } - ctx := r.Context() ctx = context.WithValue(ctx, ApiTokenScopes, []string{}) @@ -16621,7 +17214,7 @@ func (siw *ServerInterfaceWrapper) DownloadSubDAGRunArtifact(w http.ResponseWrit r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params DownloadSubDAGRunArtifactParams + var params TerminateDAGRunParams // ------------- Optional query parameter "remoteNode" ------------- @@ -16636,21 +17229,8 @@ func (siw *ServerInterfaceWrapper) DownloadSubDAGRunArtifact(w http.ResponseWrit return } - // ------------- Required query parameter "path" ------------- - - err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "", Format: ""}) - if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "path"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) - } - return - } - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.DownloadSubDAGRunArtifact(w, r, name, dagRunId, subDAGRunId, params) + siw.Handler.TerminateDAGRun(w, r, name, dagRunId, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -16660,8 +17240,8 @@ func (siw *ServerInterfaceWrapper) DownloadSubDAGRunArtifact(w http.ResponseWrit handler.ServeHTTP(w, r) } -// GetSubDAGRunArtifactPreview operation middleware -func (siw *ServerInterfaceWrapper) GetSubDAGRunArtifactPreview(w http.ResponseWriter, r *http.Request) { +// GetSubDAGRuns operation middleware +func (siw *ServerInterfaceWrapper) GetSubDAGRuns(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -16684,15 +17264,6 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunArtifactPreview(w http.ResponseWr return } - // ------------- Path parameter "subDAGRunId" ------------- - var subDAGRunId string - - err = runtime.BindStyledParameterWithOptions("simple", "subDAGRunId", chi.URLParam(r, "subDAGRunId"), &subDAGRunId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "subDAGRunId", Err: err}) - return - } - ctx := r.Context() ctx = context.WithValue(ctx, ApiTokenScopes, []string{}) @@ -16702,7 +17273,7 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunArtifactPreview(w http.ResponseWr r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params GetSubDAGRunArtifactPreviewParams + var params GetSubDAGRunsParams // ------------- Optional query parameter "remoteNode" ------------- @@ -16717,21 +17288,21 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunArtifactPreview(w http.ResponseWr return } - // ------------- Required query parameter "path" ------------- + // ------------- Optional query parameter "parentSubDAGRunId" ------------- - err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "", Format: ""}) + err = runtime.BindQueryParameterWithOptions("form", true, false, "parentSubDAGRunId", r.URL.Query(), ¶ms.ParentSubDAGRunId, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) if err != nil { var requiredError *runtime.RequiredParameterError if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "path"}) + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "parentSubDAGRunId"}) } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "parentSubDAGRunId", Err: err}) } return } handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetSubDAGRunArtifactPreview(w, r, name, dagRunId, subDAGRunId, params) + siw.Handler.GetSubDAGRuns(w, r, name, dagRunId, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -16741,8 +17312,8 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunArtifactPreview(w http.ResponseWr handler.ServeHTTP(w, r) } -// GetSubDAGRunLog operation middleware -func (siw *ServerInterfaceWrapper) GetSubDAGRunLog(w http.ResponseWriter, r *http.Request) { +// GetSubDAGRunDetails operation middleware +func (siw *ServerInterfaceWrapper) GetSubDAGRunDetails(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -16783,7 +17354,7 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunLog(w http.ResponseWriter, r *htt r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params GetSubDAGRunLogParams + var params GetSubDAGRunDetailsParams // ------------- Optional query parameter "remoteNode" ------------- @@ -16798,60 +17369,8 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunLog(w http.ResponseWriter, r *htt return } - // ------------- Optional query parameter "tail" ------------- - - err = runtime.BindQueryParameterWithOptions("form", true, false, "tail", r.URL.Query(), ¶ms.Tail, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) - if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "tail"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "tail", Err: err}) - } - return - } - - // ------------- Optional query parameter "head" ------------- - - err = runtime.BindQueryParameterWithOptions("form", true, false, "head", r.URL.Query(), ¶ms.Head, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) - if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "head"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "head", Err: err}) - } - return - } - - // ------------- Optional query parameter "offset" ------------- - - err = runtime.BindQueryParameterWithOptions("form", true, false, "offset", r.URL.Query(), ¶ms.Offset, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) - if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "offset"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "offset", Err: err}) - } - return - } - - // ------------- Optional query parameter "limit" ------------- - - err = runtime.BindQueryParameterWithOptions("form", true, false, "limit", r.URL.Query(), ¶ms.Limit, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) - if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "limit"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err}) - } - return - } - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetSubDAGRunLog(w, r, name, dagRunId, subDAGRunId, params) + siw.Handler.GetSubDAGRunDetails(w, r, name, dagRunId, subDAGRunId, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -16861,8 +17380,8 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunLog(w http.ResponseWriter, r *htt handler.ServeHTTP(w, r) } -// DownloadSubDAGRunLog operation middleware -func (siw *ServerInterfaceWrapper) DownloadSubDAGRunLog(w http.ResponseWriter, r *http.Request) { +// GetSubDAGRunArtifacts operation middleware +func (siw *ServerInterfaceWrapper) GetSubDAGRunArtifacts(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -16903,7 +17422,7 @@ func (siw *ServerInterfaceWrapper) DownloadSubDAGRunLog(w http.ResponseWriter, r r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params DownloadSubDAGRunLogParams + var params GetSubDAGRunArtifactsParams // ------------- Optional query parameter "remoteNode" ------------- @@ -16918,8 +17437,21 @@ func (siw *ServerInterfaceWrapper) DownloadSubDAGRunLog(w http.ResponseWriter, r return } + // ------------- Optional query parameter "recursive" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "recursive", r.URL.Query(), ¶ms.Recursive, runtime.BindQueryParameterOptions{Type: "boolean", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "recursive"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "recursive", Err: err}) + } + return + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.DownloadSubDAGRunLog(w, r, name, dagRunId, subDAGRunId, params) + siw.Handler.GetSubDAGRunArtifacts(w, r, name, dagRunId, subDAGRunId, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -16929,8 +17461,8 @@ func (siw *ServerInterfaceWrapper) DownloadSubDAGRunLog(w http.ResponseWriter, r handler.ServeHTTP(w, r) } -// GetSubDAGRunSpec operation middleware -func (siw *ServerInterfaceWrapper) GetSubDAGRunSpec(w http.ResponseWriter, r *http.Request) { +// DownloadSubDAGRunArtifact operation middleware +func (siw *ServerInterfaceWrapper) DownloadSubDAGRunArtifact(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -16971,7 +17503,7 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunSpec(w http.ResponseWriter, r *ht r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params GetSubDAGRunSpecParams + var params DownloadSubDAGRunArtifactParams // ------------- Optional query parameter "remoteNode" ------------- @@ -16986,8 +17518,21 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunSpec(w http.ResponseWriter, r *ht return } + // ------------- Required query parameter "path" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "path"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) + } + return + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetSubDAGRunSpec(w, r, name, dagRunId, subDAGRunId, params) + siw.Handler.DownloadSubDAGRunArtifact(w, r, name, dagRunId, subDAGRunId, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -16997,8 +17542,8 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunSpec(w http.ResponseWriter, r *ht handler.ServeHTTP(w, r) } -// DownloadSubDAGRunStepLogs operation middleware -func (siw *ServerInterfaceWrapper) DownloadSubDAGRunStepLogs(w http.ResponseWriter, r *http.Request) { +// GetSubDAGRunArtifactPreview operation middleware +func (siw *ServerInterfaceWrapper) GetSubDAGRunArtifactPreview(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -17039,7 +17584,7 @@ func (siw *ServerInterfaceWrapper) DownloadSubDAGRunStepLogs(w http.ResponseWrit r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params DownloadSubDAGRunStepLogsParams + var params GetSubDAGRunArtifactPreviewParams // ------------- Optional query parameter "remoteNode" ------------- @@ -17054,8 +17599,21 @@ func (siw *ServerInterfaceWrapper) DownloadSubDAGRunStepLogs(w http.ResponseWrit return } + // ------------- Required query parameter "path" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, true, "path", r.URL.Query(), ¶ms.Path, runtime.BindQueryParameterOptions{Type: "", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "path"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) + } + return + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.DownloadSubDAGRunStepLogs(w, r, name, dagRunId, subDAGRunId, params) + siw.Handler.GetSubDAGRunArtifactPreview(w, r, name, dagRunId, subDAGRunId, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -17065,8 +17623,8 @@ func (siw *ServerInterfaceWrapper) DownloadSubDAGRunStepLogs(w http.ResponseWrit handler.ServeHTTP(w, r) } -// DownloadSubDAGRunStepLogsForm operation middleware -func (siw *ServerInterfaceWrapper) DownloadSubDAGRunStepLogsForm(w http.ResponseWriter, r *http.Request) { +// GetSubDAGRunLog operation middleware +func (siw *ServerInterfaceWrapper) GetSubDAGRunLog(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -17107,7 +17665,7 @@ func (siw *ServerInterfaceWrapper) DownloadSubDAGRunStepLogsForm(w http.Response r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params DownloadSubDAGRunStepLogsFormParams + var params GetSubDAGRunLogParams // ------------- Optional query parameter "remoteNode" ------------- @@ -17122,94 +17680,60 @@ func (siw *ServerInterfaceWrapper) DownloadSubDAGRunStepLogsForm(w http.Response return } - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.DownloadSubDAGRunStepLogsForm(w, r, name, dagRunId, subDAGRunId, params) - })) - - for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { - handler = siw.HandlerMiddlewares[i](handler) - } - - handler.ServeHTTP(w, r) -} - -// RespondSubDAGRunStepAgentInteraction operation middleware -func (siw *ServerInterfaceWrapper) RespondSubDAGRunStepAgentInteraction(w http.ResponseWriter, r *http.Request) { - - var err error - _ = err - - // ------------- Path parameter "name" ------------- - var name DAGName - - err = runtime.BindStyledParameterWithOptions("simple", "name", chi.URLParam(r, "name"), &name, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "regex"}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "name", Err: err}) - return - } - - // ------------- Path parameter "dagRunId" ------------- - var dagRunId DAGRunId - - err = runtime.BindStyledParameterWithOptions("simple", "dagRunId", chi.URLParam(r, "dagRunId"), &dagRunId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "dagRunId", Err: err}) - return - } - - // ------------- Path parameter "subDAGRunId" ------------- - var subDAGRunId string + // ------------- Optional query parameter "tail" ------------- - err = runtime.BindStyledParameterWithOptions("simple", "subDAGRunId", chi.URLParam(r, "subDAGRunId"), &subDAGRunId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + err = runtime.BindQueryParameterWithOptions("form", true, false, "tail", r.URL.Query(), ¶ms.Tail, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "subDAGRunId", Err: err}) + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "tail"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "tail", Err: err}) + } return } - // ------------- Path parameter "stepName" ------------- - var stepName StepName + // ------------- Optional query parameter "head" ------------- - err = runtime.BindStyledParameterWithOptions("simple", "stepName", chi.URLParam(r, "stepName"), &stepName, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + err = runtime.BindQueryParameterWithOptions("form", true, false, "head", r.URL.Query(), ¶ms.Head, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stepName", Err: err}) + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "head"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "head", Err: err}) + } return } - // ------------- Path parameter "interactionId" ------------- - var interactionId string + // ------------- Optional query parameter "offset" ------------- - err = runtime.BindStyledParameterWithOptions("simple", "interactionId", chi.URLParam(r, "interactionId"), &interactionId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + err = runtime.BindQueryParameterWithOptions("form", true, false, "offset", r.URL.Query(), ¶ms.Offset, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "interactionId", Err: err}) + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "offset"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "offset", Err: err}) + } return } - ctx := r.Context() - - ctx = context.WithValue(ctx, ApiTokenScopes, []string{}) - - ctx = context.WithValue(ctx, BasicAuthScopes, []string{}) - - r = r.WithContext(ctx) - - // Parameter object where we will unmarshal all parameters from the context - var params RespondSubDAGRunStepAgentInteractionParams - - // ------------- Optional query parameter "remoteNode" ------------- + // ------------- Optional query parameter "limit" ------------- - err = runtime.BindQueryParameterWithOptions("form", true, false, "remoteNode", r.URL.Query(), ¶ms.RemoteNode, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + err = runtime.BindQueryParameterWithOptions("form", true, false, "limit", r.URL.Query(), ¶ms.Limit, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) if err != nil { var requiredError *runtime.RequiredParameterError if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "remoteNode"}) + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "limit"}) } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "remoteNode", Err: err}) + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err}) } return } handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.RespondSubDAGRunStepAgentInteraction(w, r, name, dagRunId, subDAGRunId, stepName, interactionId, params) + siw.Handler.GetSubDAGRunLog(w, r, name, dagRunId, subDAGRunId, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -17219,8 +17743,8 @@ func (siw *ServerInterfaceWrapper) RespondSubDAGRunStepAgentInteraction(w http.R handler.ServeHTTP(w, r) } -// RestartSubDAGRunStepAgentSession operation middleware -func (siw *ServerInterfaceWrapper) RestartSubDAGRunStepAgentSession(w http.ResponseWriter, r *http.Request) { +// DownloadSubDAGRunLog operation middleware +func (siw *ServerInterfaceWrapper) DownloadSubDAGRunLog(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -17252,15 +17776,6 @@ func (siw *ServerInterfaceWrapper) RestartSubDAGRunStepAgentSession(w http.Respo return } - // ------------- Path parameter "stepName" ------------- - var stepName StepName - - err = runtime.BindStyledParameterWithOptions("simple", "stepName", chi.URLParam(r, "stepName"), &stepName, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stepName", Err: err}) - return - } - ctx := r.Context() ctx = context.WithValue(ctx, ApiTokenScopes, []string{}) @@ -17270,7 +17785,7 @@ func (siw *ServerInterfaceWrapper) RestartSubDAGRunStepAgentSession(w http.Respo r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params RestartSubDAGRunStepAgentSessionParams + var params DownloadSubDAGRunLogParams // ------------- Optional query parameter "remoteNode" ------------- @@ -17286,7 +17801,7 @@ func (siw *ServerInterfaceWrapper) RestartSubDAGRunStepAgentSession(w http.Respo } handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.RestartSubDAGRunStepAgentSession(w, r, name, dagRunId, subDAGRunId, stepName, params) + siw.Handler.DownloadSubDAGRunLog(w, r, name, dagRunId, subDAGRunId, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -17296,8 +17811,8 @@ func (siw *ServerInterfaceWrapper) RestartSubDAGRunStepAgentSession(w http.Respo handler.ServeHTTP(w, r) } -// ApproveSubDAGRunStep operation middleware -func (siw *ServerInterfaceWrapper) ApproveSubDAGRunStep(w http.ResponseWriter, r *http.Request) { +// GetSubDAGRunSpec operation middleware +func (siw *ServerInterfaceWrapper) GetSubDAGRunSpec(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -17329,15 +17844,6 @@ func (siw *ServerInterfaceWrapper) ApproveSubDAGRunStep(w http.ResponseWriter, r return } - // ------------- Path parameter "stepName" ------------- - var stepName StepName - - err = runtime.BindStyledParameterWithOptions("simple", "stepName", chi.URLParam(r, "stepName"), &stepName, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stepName", Err: err}) - return - } - ctx := r.Context() ctx = context.WithValue(ctx, ApiTokenScopes, []string{}) @@ -17347,7 +17853,7 @@ func (siw *ServerInterfaceWrapper) ApproveSubDAGRunStep(w http.ResponseWriter, r r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params ApproveSubDAGRunStepParams + var params GetSubDAGRunSpecParams // ------------- Optional query parameter "remoteNode" ------------- @@ -17363,7 +17869,7 @@ func (siw *ServerInterfaceWrapper) ApproveSubDAGRunStep(w http.ResponseWriter, r } handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.ApproveSubDAGRunStep(w, r, name, dagRunId, subDAGRunId, stepName, params) + siw.Handler.GetSubDAGRunSpec(w, r, name, dagRunId, subDAGRunId, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -17373,8 +17879,8 @@ func (siw *ServerInterfaceWrapper) ApproveSubDAGRunStep(w http.ResponseWriter, r handler.ServeHTTP(w, r) } -// GetSubDAGRunForeachItems operation middleware -func (siw *ServerInterfaceWrapper) GetSubDAGRunForeachItems(w http.ResponseWriter, r *http.Request) { +// DownloadSubDAGRunStepLogs operation middleware +func (siw *ServerInterfaceWrapper) DownloadSubDAGRunStepLogs(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -17406,15 +17912,6 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunForeachItems(w http.ResponseWrite return } - // ------------- Path parameter "stepName" ------------- - var stepName StepName - - err = runtime.BindStyledParameterWithOptions("simple", "stepName", chi.URLParam(r, "stepName"), &stepName, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stepName", Err: err}) - return - } - ctx := r.Context() ctx = context.WithValue(ctx, ApiTokenScopes, []string{}) @@ -17424,7 +17921,7 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunForeachItems(w http.ResponseWrite r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params GetSubDAGRunForeachItemsParams + var params DownloadSubDAGRunStepLogsParams // ------------- Optional query parameter "remoteNode" ------------- @@ -17439,60 +17936,8 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunForeachItems(w http.ResponseWrite return } - // ------------- Optional query parameter "parent" ------------- - - err = runtime.BindQueryParameterWithOptions("form", true, false, "parent", r.URL.Query(), ¶ms.Parent, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) - if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "parent"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "parent", Err: err}) - } - return - } - - // ------------- Optional query parameter "status" ------------- - - err = runtime.BindQueryParameterWithOptions("form", true, false, "status", r.URL.Query(), ¶ms.Status, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) - if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "status"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "status", Err: err}) - } - return - } - - // ------------- Optional query parameter "page" ------------- - - err = runtime.BindQueryParameterWithOptions("form", true, false, "page", r.URL.Query(), ¶ms.Page, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) - if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "page"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "page", Err: err}) - } - return - } - - // ------------- Optional query parameter "perPage" ------------- - - err = runtime.BindQueryParameterWithOptions("form", true, false, "perPage", r.URL.Query(), ¶ms.PerPage, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) - if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "perPage"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "perPage", Err: err}) - } - return - } - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetSubDAGRunForeachItems(w, r, name, dagRunId, subDAGRunId, stepName, params) + siw.Handler.DownloadSubDAGRunStepLogs(w, r, name, dagRunId, subDAGRunId, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -17502,8 +17947,8 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunForeachItems(w http.ResponseWrite handler.ServeHTTP(w, r) } -// GetSubDAGRunForeachItem operation middleware -func (siw *ServerInterfaceWrapper) GetSubDAGRunForeachItem(w http.ResponseWriter, r *http.Request) { +// DownloadSubDAGRunStepLogsForm operation middleware +func (siw *ServerInterfaceWrapper) DownloadSubDAGRunStepLogsForm(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -17535,24 +17980,6 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunForeachItem(w http.ResponseWriter return } - // ------------- Path parameter "stepName" ------------- - var stepName StepName - - err = runtime.BindStyledParameterWithOptions("simple", "stepName", chi.URLParam(r, "stepName"), &stepName, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stepName", Err: err}) - return - } - - // ------------- Path parameter "item" ------------- - var item ForeachItemPath - - err = runtime.BindStyledParameterWithOptions("simple", "item", chi.URLParam(r, "item"), &item, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "item", Err: err}) - return - } - ctx := r.Context() ctx = context.WithValue(ctx, ApiTokenScopes, []string{}) @@ -17562,7 +17989,7 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunForeachItem(w http.ResponseWriter r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params GetSubDAGRunForeachItemParams + var params DownloadSubDAGRunStepLogsFormParams // ------------- Optional query parameter "remoteNode" ------------- @@ -17578,7 +18005,7 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunForeachItem(w http.ResponseWriter } handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetSubDAGRunForeachItem(w, r, name, dagRunId, subDAGRunId, stepName, item, params) + siw.Handler.DownloadSubDAGRunStepLogsForm(w, r, name, dagRunId, subDAGRunId, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -17588,8 +18015,8 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunForeachItem(w http.ResponseWriter handler.ServeHTTP(w, r) } -// GetSubDAGRunForeachStepLog operation middleware -func (siw *ServerInterfaceWrapper) GetSubDAGRunForeachStepLog(w http.ResponseWriter, r *http.Request) { +// RespondSubDAGRunStepAgentInteraction operation middleware +func (siw *ServerInterfaceWrapper) RespondSubDAGRunStepAgentInteraction(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -17630,21 +18057,12 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunForeachStepLog(w http.ResponseWri return } - // ------------- Path parameter "item" ------------- - var item ForeachItemPath - - err = runtime.BindStyledParameterWithOptions("simple", "item", chi.URLParam(r, "item"), &item, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "item", Err: err}) - return - } - - // ------------- Path parameter "bodyStepName" ------------- - var bodyStepName BodyStepName + // ------------- Path parameter "interactionId" ------------- + var interactionId string - err = runtime.BindStyledParameterWithOptions("simple", "bodyStepName", chi.URLParam(r, "bodyStepName"), &bodyStepName, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + err = runtime.BindStyledParameterWithOptions("simple", "interactionId", chi.URLParam(r, "interactionId"), &interactionId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "bodyStepName", Err: err}) + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "interactionId", Err: err}) return } @@ -17657,7 +18075,7 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunForeachStepLog(w http.ResponseWri r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params GetSubDAGRunForeachStepLogParams + var params RespondSubDAGRunStepAgentInteractionParams // ------------- Optional query parameter "remoteNode" ------------- @@ -17672,73 +18090,85 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunForeachStepLog(w http.ResponseWri return } - // ------------- Optional query parameter "tail" ------------- + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.RespondSubDAGRunStepAgentInteraction(w, r, name, dagRunId, subDAGRunId, stepName, interactionId, params) + })) - err = runtime.BindQueryParameterWithOptions("form", true, false, "tail", r.URL.Query(), ¶ms.Tail, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + handler = siw.HandlerMiddlewares[i](handler) + } + + handler.ServeHTTP(w, r) +} + +// RestartSubDAGRunStepAgentSession operation middleware +func (siw *ServerInterfaceWrapper) RestartSubDAGRunStepAgentSession(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "name" ------------- + var name DAGName + + err = runtime.BindStyledParameterWithOptions("simple", "name", chi.URLParam(r, "name"), &name, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "regex"}) if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "tail"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "tail", Err: err}) - } + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "name", Err: err}) return } - // ------------- Optional query parameter "head" ------------- + // ------------- Path parameter "dagRunId" ------------- + var dagRunId DAGRunId - err = runtime.BindQueryParameterWithOptions("form", true, false, "head", r.URL.Query(), ¶ms.Head, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + err = runtime.BindStyledParameterWithOptions("simple", "dagRunId", chi.URLParam(r, "dagRunId"), &dagRunId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "head"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "head", Err: err}) - } + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "dagRunId", Err: err}) return } - // ------------- Optional query parameter "offset" ------------- + // ------------- Path parameter "subDAGRunId" ------------- + var subDAGRunId string - err = runtime.BindQueryParameterWithOptions("form", true, false, "offset", r.URL.Query(), ¶ms.Offset, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + err = runtime.BindStyledParameterWithOptions("simple", "subDAGRunId", chi.URLParam(r, "subDAGRunId"), &subDAGRunId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "offset"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "offset", Err: err}) - } + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "subDAGRunId", Err: err}) return } - // ------------- Optional query parameter "limit" ------------- + // ------------- Path parameter "stepName" ------------- + var stepName StepName - err = runtime.BindQueryParameterWithOptions("form", true, false, "limit", r.URL.Query(), ¶ms.Limit, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + err = runtime.BindStyledParameterWithOptions("simple", "stepName", chi.URLParam(r, "stepName"), &stepName, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "limit"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err}) - } + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stepName", Err: err}) return } - // ------------- Optional query parameter "stream" ------------- + ctx := r.Context() - err = runtime.BindQueryParameterWithOptions("form", true, false, "stream", r.URL.Query(), ¶ms.Stream, runtime.BindQueryParameterOptions{Type: "string", Format: "string"}) + ctx = context.WithValue(ctx, ApiTokenScopes, []string{}) + + ctx = context.WithValue(ctx, BasicAuthScopes, []string{}) + + r = r.WithContext(ctx) + + // Parameter object where we will unmarshal all parameters from the context + var params RestartSubDAGRunStepAgentSessionParams + + // ------------- Optional query parameter "remoteNode" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "remoteNode", r.URL.Query(), ¶ms.RemoteNode, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) if err != nil { var requiredError *runtime.RequiredParameterError if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "stream"}) + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "remoteNode"}) } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stream", Err: err}) + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "remoteNode", Err: err}) } return } handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetSubDAGRunForeachStepLog(w, r, name, dagRunId, subDAGRunId, stepName, item, bodyStepName, params) + siw.Handler.RestartSubDAGRunStepAgentSession(w, r, name, dagRunId, subDAGRunId, stepName, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -17748,8 +18178,8 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunForeachStepLog(w http.ResponseWri handler.ServeHTTP(w, r) } -// DownloadSubDAGRunForeachStepLog operation middleware -func (siw *ServerInterfaceWrapper) DownloadSubDAGRunForeachStepLog(w http.ResponseWriter, r *http.Request) { +// ApproveSubDAGRunStep operation middleware +func (siw *ServerInterfaceWrapper) ApproveSubDAGRunStep(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -17790,24 +18220,6 @@ func (siw *ServerInterfaceWrapper) DownloadSubDAGRunForeachStepLog(w http.Respon return } - // ------------- Path parameter "item" ------------- - var item ForeachItemPath - - err = runtime.BindStyledParameterWithOptions("simple", "item", chi.URLParam(r, "item"), &item, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "item", Err: err}) - return - } - - // ------------- Path parameter "bodyStepName" ------------- - var bodyStepName BodyStepName - - err = runtime.BindStyledParameterWithOptions("simple", "bodyStepName", chi.URLParam(r, "bodyStepName"), &bodyStepName, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "bodyStepName", Err: err}) - return - } - ctx := r.Context() ctx = context.WithValue(ctx, ApiTokenScopes, []string{}) @@ -17817,7 +18229,7 @@ func (siw *ServerInterfaceWrapper) DownloadSubDAGRunForeachStepLog(w http.Respon r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params DownloadSubDAGRunForeachStepLogParams + var params ApproveSubDAGRunStepParams // ------------- Optional query parameter "remoteNode" ------------- @@ -17832,21 +18244,8 @@ func (siw *ServerInterfaceWrapper) DownloadSubDAGRunForeachStepLog(w http.Respon return } - // ------------- Optional query parameter "stream" ------------- - - err = runtime.BindQueryParameterWithOptions("form", true, false, "stream", r.URL.Query(), ¶ms.Stream, runtime.BindQueryParameterOptions{Type: "string", Format: "string"}) - if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "stream"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stream", Err: err}) - } - return - } - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.DownloadSubDAGRunForeachStepLog(w, r, name, dagRunId, subDAGRunId, stepName, item, bodyStepName, params) + siw.Handler.ApproveSubDAGRunStep(w, r, name, dagRunId, subDAGRunId, stepName, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -17856,8 +18255,8 @@ func (siw *ServerInterfaceWrapper) DownloadSubDAGRunForeachStepLog(w http.Respon handler.ServeHTTP(w, r) } -// GetSubDAGRunStepLog operation middleware -func (siw *ServerInterfaceWrapper) GetSubDAGRunStepLog(w http.ResponseWriter, r *http.Request) { +// GetSubDAGRunForeachItems operation middleware +func (siw *ServerInterfaceWrapper) GetSubDAGRunForeachItems(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -17907,7 +18306,7 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunStepLog(w http.ResponseWriter, r r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params GetSubDAGRunStepLogParams + var params GetSubDAGRunForeachItemsParams // ------------- Optional query parameter "remoteNode" ------------- @@ -17922,73 +18321,60 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunStepLog(w http.ResponseWriter, r return } - // ------------- Optional query parameter "tail" ------------- - - err = runtime.BindQueryParameterWithOptions("form", true, false, "tail", r.URL.Query(), ¶ms.Tail, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) - if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "tail"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "tail", Err: err}) - } - return - } - - // ------------- Optional query parameter "head" ------------- + // ------------- Optional query parameter "parent" ------------- - err = runtime.BindQueryParameterWithOptions("form", true, false, "head", r.URL.Query(), ¶ms.Head, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + err = runtime.BindQueryParameterWithOptions("form", true, false, "parent", r.URL.Query(), ¶ms.Parent, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) if err != nil { var requiredError *runtime.RequiredParameterError if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "head"}) + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "parent"}) } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "head", Err: err}) + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "parent", Err: err}) } return } - // ------------- Optional query parameter "offset" ------------- + // ------------- Optional query parameter "status" ------------- - err = runtime.BindQueryParameterWithOptions("form", true, false, "offset", r.URL.Query(), ¶ms.Offset, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + err = runtime.BindQueryParameterWithOptions("form", true, false, "status", r.URL.Query(), ¶ms.Status, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) if err != nil { var requiredError *runtime.RequiredParameterError if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "offset"}) + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "status"}) } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "offset", Err: err}) + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "status", Err: err}) } return } - // ------------- Optional query parameter "limit" ------------- + // ------------- Optional query parameter "page" ------------- - err = runtime.BindQueryParameterWithOptions("form", true, false, "limit", r.URL.Query(), ¶ms.Limit, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + err = runtime.BindQueryParameterWithOptions("form", true, false, "page", r.URL.Query(), ¶ms.Page, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) if err != nil { var requiredError *runtime.RequiredParameterError if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "limit"}) + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "page"}) } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err}) + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "page", Err: err}) } return } - // ------------- Optional query parameter "stream" ------------- + // ------------- Optional query parameter "perPage" ------------- - err = runtime.BindQueryParameterWithOptions("form", true, false, "stream", r.URL.Query(), ¶ms.Stream, runtime.BindQueryParameterOptions{Type: "string", Format: "string"}) + err = runtime.BindQueryParameterWithOptions("form", true, false, "perPage", r.URL.Query(), ¶ms.PerPage, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) if err != nil { var requiredError *runtime.RequiredParameterError if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "stream"}) + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "perPage"}) } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stream", Err: err}) + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "perPage", Err: err}) } return } handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetSubDAGRunStepLog(w, r, name, dagRunId, subDAGRunId, stepName, params) + siw.Handler.GetSubDAGRunForeachItems(w, r, name, dagRunId, subDAGRunId, stepName, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -17998,8 +18384,8 @@ func (siw *ServerInterfaceWrapper) GetSubDAGRunStepLog(w http.ResponseWriter, r handler.ServeHTTP(w, r) } -// DownloadSubDAGRunStepLog operation middleware -func (siw *ServerInterfaceWrapper) DownloadSubDAGRunStepLog(w http.ResponseWriter, r *http.Request) { +// GetSubDAGRunForeachItem operation middleware +func (siw *ServerInterfaceWrapper) GetSubDAGRunForeachItem(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -18040,6 +18426,15 @@ func (siw *ServerInterfaceWrapper) DownloadSubDAGRunStepLog(w http.ResponseWrite return } + // ------------- Path parameter "item" ------------- + var item ForeachItemPath + + err = runtime.BindStyledParameterWithOptions("simple", "item", chi.URLParam(r, "item"), &item, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "item", Err: err}) + return + } + ctx := r.Context() ctx = context.WithValue(ctx, ApiTokenScopes, []string{}) @@ -18049,7 +18444,7 @@ func (siw *ServerInterfaceWrapper) DownloadSubDAGRunStepLog(w http.ResponseWrite r = r.WithContext(ctx) // Parameter object where we will unmarshal all parameters from the context - var params DownloadSubDAGRunStepLogParams + var params GetSubDAGRunForeachItemParams // ------------- Optional query parameter "remoteNode" ------------- @@ -18064,21 +18459,8 @@ func (siw *ServerInterfaceWrapper) DownloadSubDAGRunStepLog(w http.ResponseWrite return } - // ------------- Optional query parameter "stream" ------------- - - err = runtime.BindQueryParameterWithOptions("form", true, false, "stream", r.URL.Query(), ¶ms.Stream, runtime.BindQueryParameterOptions{Type: "string", Format: "string"}) - if err != nil { - var requiredError *runtime.RequiredParameterError - if errors.As(err, &requiredError) { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "stream"}) - } else { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stream", Err: err}) - } - return - } - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.DownloadSubDAGRunStepLog(w, r, name, dagRunId, subDAGRunId, stepName, params) + siw.Handler.GetSubDAGRunForeachItem(w, r, name, dagRunId, subDAGRunId, stepName, item, params) })) for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -18088,8 +18470,508 @@ func (siw *ServerInterfaceWrapper) DownloadSubDAGRunStepLog(w http.ResponseWrite handler.ServeHTTP(w, r) } -// GetSubDAGRunStepMessages operation middleware -func (siw *ServerInterfaceWrapper) GetSubDAGRunStepMessages(w http.ResponseWriter, r *http.Request) { +// GetSubDAGRunForeachStepLog operation middleware +func (siw *ServerInterfaceWrapper) GetSubDAGRunForeachStepLog(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "name" ------------- + var name DAGName + + err = runtime.BindStyledParameterWithOptions("simple", "name", chi.URLParam(r, "name"), &name, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "regex"}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "name", Err: err}) + return + } + + // ------------- Path parameter "dagRunId" ------------- + var dagRunId DAGRunId + + err = runtime.BindStyledParameterWithOptions("simple", "dagRunId", chi.URLParam(r, "dagRunId"), &dagRunId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "dagRunId", Err: err}) + return + } + + // ------------- Path parameter "subDAGRunId" ------------- + var subDAGRunId string + + err = runtime.BindStyledParameterWithOptions("simple", "subDAGRunId", chi.URLParam(r, "subDAGRunId"), &subDAGRunId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "subDAGRunId", Err: err}) + return + } + + // ------------- Path parameter "stepName" ------------- + var stepName StepName + + err = runtime.BindStyledParameterWithOptions("simple", "stepName", chi.URLParam(r, "stepName"), &stepName, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stepName", Err: err}) + return + } + + // ------------- Path parameter "item" ------------- + var item ForeachItemPath + + err = runtime.BindStyledParameterWithOptions("simple", "item", chi.URLParam(r, "item"), &item, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "item", Err: err}) + return + } + + // ------------- Path parameter "bodyStepName" ------------- + var bodyStepName BodyStepName + + err = runtime.BindStyledParameterWithOptions("simple", "bodyStepName", chi.URLParam(r, "bodyStepName"), &bodyStepName, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "bodyStepName", Err: err}) + return + } + + ctx := r.Context() + + ctx = context.WithValue(ctx, ApiTokenScopes, []string{}) + + ctx = context.WithValue(ctx, BasicAuthScopes, []string{}) + + r = r.WithContext(ctx) + + // Parameter object where we will unmarshal all parameters from the context + var params GetSubDAGRunForeachStepLogParams + + // ------------- Optional query parameter "remoteNode" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "remoteNode", r.URL.Query(), ¶ms.RemoteNode, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "remoteNode"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "remoteNode", Err: err}) + } + return + } + + // ------------- Optional query parameter "tail" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "tail", r.URL.Query(), ¶ms.Tail, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "tail"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "tail", Err: err}) + } + return + } + + // ------------- Optional query parameter "head" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "head", r.URL.Query(), ¶ms.Head, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "head"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "head", Err: err}) + } + return + } + + // ------------- Optional query parameter "offset" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "offset", r.URL.Query(), ¶ms.Offset, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "offset"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "offset", Err: err}) + } + return + } + + // ------------- Optional query parameter "limit" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "limit", r.URL.Query(), ¶ms.Limit, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "limit"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err}) + } + return + } + + // ------------- Optional query parameter "stream" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "stream", r.URL.Query(), ¶ms.Stream, runtime.BindQueryParameterOptions{Type: "string", Format: "string"}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "stream"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stream", Err: err}) + } + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetSubDAGRunForeachStepLog(w, r, name, dagRunId, subDAGRunId, stepName, item, bodyStepName, params) + })) + + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + handler = siw.HandlerMiddlewares[i](handler) + } + + handler.ServeHTTP(w, r) +} + +// DownloadSubDAGRunForeachStepLog operation middleware +func (siw *ServerInterfaceWrapper) DownloadSubDAGRunForeachStepLog(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "name" ------------- + var name DAGName + + err = runtime.BindStyledParameterWithOptions("simple", "name", chi.URLParam(r, "name"), &name, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "regex"}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "name", Err: err}) + return + } + + // ------------- Path parameter "dagRunId" ------------- + var dagRunId DAGRunId + + err = runtime.BindStyledParameterWithOptions("simple", "dagRunId", chi.URLParam(r, "dagRunId"), &dagRunId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "dagRunId", Err: err}) + return + } + + // ------------- Path parameter "subDAGRunId" ------------- + var subDAGRunId string + + err = runtime.BindStyledParameterWithOptions("simple", "subDAGRunId", chi.URLParam(r, "subDAGRunId"), &subDAGRunId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "subDAGRunId", Err: err}) + return + } + + // ------------- Path parameter "stepName" ------------- + var stepName StepName + + err = runtime.BindStyledParameterWithOptions("simple", "stepName", chi.URLParam(r, "stepName"), &stepName, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stepName", Err: err}) + return + } + + // ------------- Path parameter "item" ------------- + var item ForeachItemPath + + err = runtime.BindStyledParameterWithOptions("simple", "item", chi.URLParam(r, "item"), &item, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "item", Err: err}) + return + } + + // ------------- Path parameter "bodyStepName" ------------- + var bodyStepName BodyStepName + + err = runtime.BindStyledParameterWithOptions("simple", "bodyStepName", chi.URLParam(r, "bodyStepName"), &bodyStepName, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "bodyStepName", Err: err}) + return + } + + ctx := r.Context() + + ctx = context.WithValue(ctx, ApiTokenScopes, []string{}) + + ctx = context.WithValue(ctx, BasicAuthScopes, []string{}) + + r = r.WithContext(ctx) + + // Parameter object where we will unmarshal all parameters from the context + var params DownloadSubDAGRunForeachStepLogParams + + // ------------- Optional query parameter "remoteNode" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "remoteNode", r.URL.Query(), ¶ms.RemoteNode, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "remoteNode"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "remoteNode", Err: err}) + } + return + } + + // ------------- Optional query parameter "stream" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "stream", r.URL.Query(), ¶ms.Stream, runtime.BindQueryParameterOptions{Type: "string", Format: "string"}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "stream"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stream", Err: err}) + } + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.DownloadSubDAGRunForeachStepLog(w, r, name, dagRunId, subDAGRunId, stepName, item, bodyStepName, params) + })) + + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + handler = siw.HandlerMiddlewares[i](handler) + } + + handler.ServeHTTP(w, r) +} + +// GetSubDAGRunStepLog operation middleware +func (siw *ServerInterfaceWrapper) GetSubDAGRunStepLog(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "name" ------------- + var name DAGName + + err = runtime.BindStyledParameterWithOptions("simple", "name", chi.URLParam(r, "name"), &name, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "regex"}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "name", Err: err}) + return + } + + // ------------- Path parameter "dagRunId" ------------- + var dagRunId DAGRunId + + err = runtime.BindStyledParameterWithOptions("simple", "dagRunId", chi.URLParam(r, "dagRunId"), &dagRunId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "dagRunId", Err: err}) + return + } + + // ------------- Path parameter "subDAGRunId" ------------- + var subDAGRunId string + + err = runtime.BindStyledParameterWithOptions("simple", "subDAGRunId", chi.URLParam(r, "subDAGRunId"), &subDAGRunId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "subDAGRunId", Err: err}) + return + } + + // ------------- Path parameter "stepName" ------------- + var stepName StepName + + err = runtime.BindStyledParameterWithOptions("simple", "stepName", chi.URLParam(r, "stepName"), &stepName, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stepName", Err: err}) + return + } + + ctx := r.Context() + + ctx = context.WithValue(ctx, ApiTokenScopes, []string{}) + + ctx = context.WithValue(ctx, BasicAuthScopes, []string{}) + + r = r.WithContext(ctx) + + // Parameter object where we will unmarshal all parameters from the context + var params GetSubDAGRunStepLogParams + + // ------------- Optional query parameter "remoteNode" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "remoteNode", r.URL.Query(), ¶ms.RemoteNode, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "remoteNode"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "remoteNode", Err: err}) + } + return + } + + // ------------- Optional query parameter "tail" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "tail", r.URL.Query(), ¶ms.Tail, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "tail"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "tail", Err: err}) + } + return + } + + // ------------- Optional query parameter "head" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "head", r.URL.Query(), ¶ms.Head, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "head"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "head", Err: err}) + } + return + } + + // ------------- Optional query parameter "offset" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "offset", r.URL.Query(), ¶ms.Offset, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "offset"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "offset", Err: err}) + } + return + } + + // ------------- Optional query parameter "limit" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "limit", r.URL.Query(), ¶ms.Limit, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "limit"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err}) + } + return + } + + // ------------- Optional query parameter "stream" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "stream", r.URL.Query(), ¶ms.Stream, runtime.BindQueryParameterOptions{Type: "string", Format: "string"}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "stream"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stream", Err: err}) + } + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetSubDAGRunStepLog(w, r, name, dagRunId, subDAGRunId, stepName, params) + })) + + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + handler = siw.HandlerMiddlewares[i](handler) + } + + handler.ServeHTTP(w, r) +} + +// DownloadSubDAGRunStepLog operation middleware +func (siw *ServerInterfaceWrapper) DownloadSubDAGRunStepLog(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "name" ------------- + var name DAGName + + err = runtime.BindStyledParameterWithOptions("simple", "name", chi.URLParam(r, "name"), &name, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "regex"}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "name", Err: err}) + return + } + + // ------------- Path parameter "dagRunId" ------------- + var dagRunId DAGRunId + + err = runtime.BindStyledParameterWithOptions("simple", "dagRunId", chi.URLParam(r, "dagRunId"), &dagRunId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "dagRunId", Err: err}) + return + } + + // ------------- Path parameter "subDAGRunId" ------------- + var subDAGRunId string + + err = runtime.BindStyledParameterWithOptions("simple", "subDAGRunId", chi.URLParam(r, "subDAGRunId"), &subDAGRunId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "subDAGRunId", Err: err}) + return + } + + // ------------- Path parameter "stepName" ------------- + var stepName StepName + + err = runtime.BindStyledParameterWithOptions("simple", "stepName", chi.URLParam(r, "stepName"), &stepName, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stepName", Err: err}) + return + } + + ctx := r.Context() + + ctx = context.WithValue(ctx, ApiTokenScopes, []string{}) + + ctx = context.WithValue(ctx, BasicAuthScopes, []string{}) + + r = r.WithContext(ctx) + + // Parameter object where we will unmarshal all parameters from the context + var params DownloadSubDAGRunStepLogParams + + // ------------- Optional query parameter "remoteNode" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "remoteNode", r.URL.Query(), ¶ms.RemoteNode, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "remoteNode"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "remoteNode", Err: err}) + } + return + } + + // ------------- Optional query parameter "stream" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "stream", r.URL.Query(), ¶ms.Stream, runtime.BindQueryParameterOptions{Type: "string", Format: "string"}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "stream"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "stream", Err: err}) + } + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.DownloadSubDAGRunStepLog(w, r, name, dagRunId, subDAGRunId, stepName, params) + })) + + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + handler = siw.HandlerMiddlewares[i](handler) + } + + handler.ServeHTTP(w, r) +} + +// GetSubDAGRunStepMessages operation middleware +func (siw *ServerInterfaceWrapper) GetSubDAGRunStepMessages(w http.ResponseWriter, r *http.Request) { var err error _ = err @@ -19000,6 +19882,19 @@ func (siw *ServerInterfaceWrapper) GetDAGDAGRunHistory(w http.ResponseWriter, r return } + // ------------- Optional query parameter "includeEphemeral" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "includeEphemeral", r.URL.Query(), ¶ms.IncludeEphemeral, runtime.BindQueryParameterOptions{Type: "boolean", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "includeEphemeral"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "includeEphemeral", Err: err}) + } + return + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { siw.Handler.GetDAGDAGRunHistory(w, r, fileName, params) })) @@ -28338,6 +29233,12 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/dag-runs/{name}/{dagRunId}/steps/{stepName}/approve", wrapper.ApproveDAGRunStep) }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/dag-runs/{name}/{dagRunId}/steps/{stepName}/availability", wrapper.GetDAGRunStepAvailability) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/dag-runs/{name}/{dagRunId}/steps/{stepName}/events", wrapper.GetDAGRunStepEvents) + }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/dag-runs/{name}/{dagRunId}/steps/{stepName}/foreach", wrapper.GetDAGRunForeachItems) }) @@ -28359,12 +29260,18 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/dag-runs/{name}/{dagRunId}/steps/{stepName}/messages", wrapper.GetDAGRunStepMessages) }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/dag-runs/{name}/{dagRunId}/steps/{stepName}/outputs", wrapper.GetDAGRunStepOutputs) + }) r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/dag-runs/{name}/{dagRunId}/steps/{stepName}/push-back", wrapper.PushBackDAGRunStep) }) r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/dag-runs/{name}/{dagRunId}/steps/{stepName}/reject", wrapper.RejectDAGRunStep) }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/dag-runs/{name}/{dagRunId}/steps/{stepName}/samples", wrapper.GetDAGRunStepSamples) + }) r.Group(func(r chi.Router) { r.Patch(options.BaseURL+"/dag-runs/{name}/{dagRunId}/steps/{stepName}/status", wrapper.UpdateDAGRunStepStatus) }) @@ -30517,6 +31424,20 @@ func (response EditRetryDAGRun200JSONResponse) VisitEditRetryDAGRunResponse(w ht return err } +type EditRetryDAGRun409JSONResponse Error + +func (response EditRetryDAGRun409JSONResponse) VisitEditRetryDAGRunResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(409) + _, err := buf.WriteTo(w) + return err +} + type EditRetryDAGRundefaultJSONResponse struct { Body Error StatusCode int @@ -30583,6 +31504,20 @@ func (response PreviewEditRetryDAGRun200JSONResponse) VisitPreviewEditRetryDAGRu return err } +type PreviewEditRetryDAGRun409JSONResponse Error + +func (response PreviewEditRetryDAGRun409JSONResponse) VisitPreviewEditRetryDAGRunResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(409) + _, err := buf.WriteTo(w) + return err +} + type PreviewEditRetryDAGRundefaultJSONResponse struct { Body Error StatusCode int @@ -31279,6 +32214,20 @@ func (response RetryDAGRun200Response) VisitRetryDAGRunResponse(w http.ResponseW return nil } +type RetryDAGRun409JSONResponse Error + +func (response RetryDAGRun409JSONResponse) VisitRetryDAGRunResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(409) + _, err := buf.WriteTo(w) + return err +} + type RetryDAGRundefaultJSONResponse struct { Body Error StatusCode int @@ -31762,6 +32711,132 @@ func (response ApproveDAGRunStepdefaultJSONResponse) VisitApproveDAGRunStepRespo return err } +type GetDAGRunStepAvailabilityRequestObject struct { + Name DAGName `json:"name"` + DagRunId DAGRunId `json:"dagRunId"` + StepName StepName `json:"stepName"` + Params GetDAGRunStepAvailabilityParams +} + +type GetDAGRunStepAvailabilityResponseObject interface { + VisitGetDAGRunStepAvailabilityResponse(w http.ResponseWriter) error +} + +type GetDAGRunStepAvailability200JSONResponse StepAvailability + +func (response GetDAGRunStepAvailability200JSONResponse) VisitGetDAGRunStepAvailabilityResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + _, err := buf.WriteTo(w) + return err +} + +type GetDAGRunStepAvailability404JSONResponse Error + +func (response GetDAGRunStepAvailability404JSONResponse) VisitGetDAGRunStepAvailabilityResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + _, err := buf.WriteTo(w) + return err +} + +type GetDAGRunStepAvailabilitydefaultJSONResponse struct { + Body Error + StatusCode int +} + +func (response GetDAGRunStepAvailabilitydefaultJSONResponse) VisitGetDAGRunStepAvailabilityResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response.Body); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(response.StatusCode) + _, err := buf.WriteTo(w) + return err +} + +type GetDAGRunStepEventsRequestObject struct { + Name DAGName `json:"name"` + DagRunId DAGRunId `json:"dagRunId"` + StepName StepName `json:"stepName"` + Params GetDAGRunStepEventsParams +} + +type GetDAGRunStepEventsResponseObject interface { + VisitGetDAGRunStepEventsResponse(w http.ResponseWriter) error +} + +type GetDAGRunStepEvents200JSONResponse StepEvents + +func (response GetDAGRunStepEvents200JSONResponse) VisitGetDAGRunStepEventsResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + _, err := buf.WriteTo(w) + return err +} + +type GetDAGRunStepEvents400JSONResponse Error + +func (response GetDAGRunStepEvents400JSONResponse) VisitGetDAGRunStepEventsResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + _, err := buf.WriteTo(w) + return err +} + +type GetDAGRunStepEvents404JSONResponse Error + +func (response GetDAGRunStepEvents404JSONResponse) VisitGetDAGRunStepEventsResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + _, err := buf.WriteTo(w) + return err +} + +type GetDAGRunStepEventsdefaultJSONResponse struct { + Body Error + StatusCode int +} + +func (response GetDAGRunStepEventsdefaultJSONResponse) VisitGetDAGRunStepEventsResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response.Body); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(response.StatusCode) + _, err := buf.WriteTo(w) + return err +} + type GetDAGRunForeachItemsRequestObject struct { Name DAGName `json:"name"` DagRunId DAGRunId `json:"dagRunId"` @@ -32173,6 +33248,62 @@ func (response GetDAGRunStepMessagesdefaultJSONResponse) VisitGetDAGRunStepMessa return err } +type GetDAGRunStepOutputsRequestObject struct { + Name DAGName `json:"name"` + DagRunId DAGRunId `json:"dagRunId"` + StepName StepName `json:"stepName"` + Params GetDAGRunStepOutputsParams +} + +type GetDAGRunStepOutputsResponseObject interface { + VisitGetDAGRunStepOutputsResponse(w http.ResponseWriter) error +} + +type GetDAGRunStepOutputs200JSONResponse StepOutputs + +func (response GetDAGRunStepOutputs200JSONResponse) VisitGetDAGRunStepOutputsResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + _, err := buf.WriteTo(w) + return err +} + +type GetDAGRunStepOutputs404JSONResponse Error + +func (response GetDAGRunStepOutputs404JSONResponse) VisitGetDAGRunStepOutputsResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + _, err := buf.WriteTo(w) + return err +} + +type GetDAGRunStepOutputsdefaultJSONResponse struct { + Body Error + StatusCode int +} + +func (response GetDAGRunStepOutputsdefaultJSONResponse) VisitGetDAGRunStepOutputsResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response.Body); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(response.StatusCode) + _, err := buf.WriteTo(w) + return err +} + type PushBackDAGRunStepRequestObject struct { Name DAGName `json:"name"` DagRunId DAGRunId `json:"dagRunId"` @@ -32315,6 +33446,76 @@ func (response RejectDAGRunStepdefaultJSONResponse) VisitRejectDAGRunStepRespons return err } +type GetDAGRunStepSamplesRequestObject struct { + Name DAGName `json:"name"` + DagRunId DAGRunId `json:"dagRunId"` + StepName StepName `json:"stepName"` + Params GetDAGRunStepSamplesParams +} + +type GetDAGRunStepSamplesResponseObject interface { + VisitGetDAGRunStepSamplesResponse(w http.ResponseWriter) error +} + +type GetDAGRunStepSamples200JSONResponse StepSamples + +func (response GetDAGRunStepSamples200JSONResponse) VisitGetDAGRunStepSamplesResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + _, err := buf.WriteTo(w) + return err +} + +type GetDAGRunStepSamples400JSONResponse Error + +func (response GetDAGRunStepSamples400JSONResponse) VisitGetDAGRunStepSamplesResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + _, err := buf.WriteTo(w) + return err +} + +type GetDAGRunStepSamples404JSONResponse Error + +func (response GetDAGRunStepSamples404JSONResponse) VisitGetDAGRunStepSamplesResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + _, err := buf.WriteTo(w) + return err +} + +type GetDAGRunStepSamplesdefaultJSONResponse struct { + Body Error + StatusCode int +} + +func (response GetDAGRunStepSamplesdefaultJSONResponse) VisitGetDAGRunStepSamplesResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response.Body); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(response.StatusCode) + _, err := buf.WriteTo(w) + return err +} + type UpdateDAGRunStepStatusRequestObject struct { Name DAGName `json:"name"` DagRunId DAGRunId `json:"dagRunId"` @@ -34146,9 +35347,18 @@ type ValidateDAGSpec200JSONResponse struct { // Errors List of validation errors Errors []string `json:"errors"` + // Issues Structured errors, warnings and notices of the spec, with step and location data when known + Issues []ValidationIssue `json:"issues"` + + // ReferenceScope Values that a step of the DAG can reference. + ReferenceScope *ReferenceScope `json:"referenceScope,omitempty"` + // Valid True if the spec is valid (no errors) Valid bool `json:"valid"` + // ValueReferenceNotices Passive value-reference notices produced while loading this spec. These notices are not persisted. + ValueReferenceNotices []ValueReferenceNotice `json:"valueReferenceNotices"` + // Warnings Non-fatal spec warnings; these do not affect valid Warnings []string `json:"warnings,omitempty"` } @@ -35382,6 +36592,9 @@ type GetDAGSpec200JSONResponse struct { // Errors List of errors in the spec Errors []string `json:"errors"` + // Sha256 Lowercase hex SHA-256 of the file bytes that spec holds + Sha256 string `json:"sha256,omitempty"` + // Spec The DAG spec in YAML format Spec string `json:"spec"` @@ -35434,6 +36647,9 @@ type UpdateDAGSpecResponseObject interface { type UpdateDAGSpec200JSONResponse struct { // Errors List of errors in the spec Errors []string `json:"errors"` + + // Sha256 Lowercase hex SHA-256 of the bytes written + Sha256 string `json:"sha256"` } func (response UpdateDAGSpec200JSONResponse) VisitUpdateDAGSpecResponse(w http.ResponseWriter) error { @@ -35448,6 +36664,20 @@ func (response UpdateDAGSpec200JSONResponse) VisitUpdateDAGSpecResponse(w http.R return err } +type UpdateDAGSpec409JSONResponse Error + +func (response UpdateDAGSpec409JSONResponse) VisitUpdateDAGSpecResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(409) + _, err := buf.WriteTo(w) + return err +} + type UpdateDAGSpecdefaultJSONResponse struct { Body Error StatusCode int @@ -46158,6 +47388,12 @@ type StrictServerInterface interface { // Approve a waiting step // (POST /dag-runs/{name}/{dagRunId}/steps/{stepName}/approve) ApproveDAGRunStep(ctx context.Context, request ApproveDAGRunStepRequestObject) (ApproveDAGRunStepResponseObject, error) + // Retrieve the availability of a continuous step + // (GET /dag-runs/{name}/{dagRunId}/steps/{stepName}/availability) + GetDAGRunStepAvailability(ctx context.Context, request GetDAGRunStepAvailabilityRequestObject) (GetDAGRunStepAvailabilityResponseObject, error) + // Retrieve the events of a continuous step + // (GET /dag-runs/{name}/{dagRunId}/steps/{stepName}/events) + GetDAGRunStepEvents(ctx context.Context, request GetDAGRunStepEventsRequestObject) (GetDAGRunStepEventsResponseObject, error) // List the items of a foreach step in a DAG-run // (GET /dag-runs/{name}/{dagRunId}/steps/{stepName}/foreach) GetDAGRunForeachItems(ctx context.Context, request GetDAGRunForeachItemsRequestObject) (GetDAGRunForeachItemsResponseObject, error) @@ -46179,12 +47415,18 @@ type StrictServerInterface interface { // Retrieve chat messages for a step // (GET /dag-runs/{name}/{dagRunId}/steps/{stepName}/messages) GetDAGRunStepMessages(ctx context.Context, request GetDAGRunStepMessagesRequestObject) (GetDAGRunStepMessagesResponseObject, error) + // Retrieve outputs of a specific step in a DAG-run + // (GET /dag-runs/{name}/{dagRunId}/steps/{stepName}/outputs) + GetDAGRunStepOutputs(ctx context.Context, request GetDAGRunStepOutputsRequestObject) (GetDAGRunStepOutputsResponseObject, error) // Push back a waiting step for re-execution with feedback // (POST /dag-runs/{name}/{dagRunId}/steps/{stepName}/push-back) PushBackDAGRunStep(ctx context.Context, request PushBackDAGRunStepRequestObject) (PushBackDAGRunStepResponseObject, error) // Reject a waiting step // (POST /dag-runs/{name}/{dagRunId}/steps/{stepName}/reject) RejectDAGRunStep(ctx context.Context, request RejectDAGRunStepRequestObject) (RejectDAGRunStepResponseObject, error) + // Retrieve the samples of a continuous step, in buckets + // (GET /dag-runs/{name}/{dagRunId}/steps/{stepName}/samples) + GetDAGRunStepSamples(ctx context.Context, request GetDAGRunStepSamplesRequestObject) (GetDAGRunStepSamplesResponseObject, error) // Manually update a step's execution status // (PATCH /dag-runs/{name}/{dagRunId}/steps/{stepName}/status) UpdateDAGRunStepStatus(ctx context.Context, request UpdateDAGRunStepStatusRequestObject) (UpdateDAGRunStepStatusResponseObject, error) @@ -48034,6 +49276,64 @@ func (sh *strictHandler) ApproveDAGRunStep(w http.ResponseWriter, r *http.Reques } } +// GetDAGRunStepAvailability operation middleware +func (sh *strictHandler) GetDAGRunStepAvailability(w http.ResponseWriter, r *http.Request, name DAGName, dagRunId DAGRunId, stepName StepName, params GetDAGRunStepAvailabilityParams) { + var request GetDAGRunStepAvailabilityRequestObject + + request.Name = name + request.DagRunId = dagRunId + request.StepName = stepName + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetDAGRunStepAvailability(ctx, request.(GetDAGRunStepAvailabilityRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetDAGRunStepAvailability") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetDAGRunStepAvailabilityResponseObject); ok { + if err := validResponse.VisitGetDAGRunStepAvailabilityResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetDAGRunStepEvents operation middleware +func (sh *strictHandler) GetDAGRunStepEvents(w http.ResponseWriter, r *http.Request, name DAGName, dagRunId DAGRunId, stepName StepName, params GetDAGRunStepEventsParams) { + var request GetDAGRunStepEventsRequestObject + + request.Name = name + request.DagRunId = dagRunId + request.StepName = stepName + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetDAGRunStepEvents(ctx, request.(GetDAGRunStepEventsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetDAGRunStepEvents") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetDAGRunStepEventsResponseObject); ok { + if err := validResponse.VisitGetDAGRunStepEventsResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // GetDAGRunForeachItems operation middleware func (sh *strictHandler) GetDAGRunForeachItems(w http.ResponseWriter, r *http.Request, name DAGName, dagRunId DAGRunId, stepName StepName, params GetDAGRunForeachItemsParams) { var request GetDAGRunForeachItemsRequestObject @@ -48242,6 +49542,35 @@ func (sh *strictHandler) GetDAGRunStepMessages(w http.ResponseWriter, r *http.Re } } +// GetDAGRunStepOutputs operation middleware +func (sh *strictHandler) GetDAGRunStepOutputs(w http.ResponseWriter, r *http.Request, name DAGName, dagRunId DAGRunId, stepName StepName, params GetDAGRunStepOutputsParams) { + var request GetDAGRunStepOutputsRequestObject + + request.Name = name + request.DagRunId = dagRunId + request.StepName = stepName + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetDAGRunStepOutputs(ctx, request.(GetDAGRunStepOutputsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetDAGRunStepOutputs") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetDAGRunStepOutputsResponseObject); ok { + if err := validResponse.VisitGetDAGRunStepOutputsResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // PushBackDAGRunStep operation middleware func (sh *strictHandler) PushBackDAGRunStep(w http.ResponseWriter, r *http.Request, name DAGName, dagRunId DAGRunId, stepName StepName, params PushBackDAGRunStepParams) { var request PushBackDAGRunStepRequestObject @@ -48320,6 +49649,35 @@ func (sh *strictHandler) RejectDAGRunStep(w http.ResponseWriter, r *http.Request } } +// GetDAGRunStepSamples operation middleware +func (sh *strictHandler) GetDAGRunStepSamples(w http.ResponseWriter, r *http.Request, name DAGName, dagRunId DAGRunId, stepName StepName, params GetDAGRunStepSamplesParams) { + var request GetDAGRunStepSamplesRequestObject + + request.Name = name + request.DagRunId = dagRunId + request.StepName = stepName + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetDAGRunStepSamples(ctx, request.(GetDAGRunStepSamplesRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetDAGRunStepSamples") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetDAGRunStepSamplesResponseObject); ok { + if err := validResponse.VisitGetDAGRunStepSamplesResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // UpdateDAGRunStepStatus operation middleware func (sh *strictHandler) UpdateDAGRunStepStatus(w http.ResponseWriter, r *http.Request, name DAGName, dagRunId DAGRunId, stepName StepName, params UpdateDAGRunStepStatusParams) { var request UpdateDAGRunStepStatusRequestObject @@ -54606,1097 +55964,1177 @@ func (sh *strictHandler) UpdateWorkspace(w http.ResponseWriter, r *http.Request, // const string: with thousands of chunks the chained `+` fold is several // times slower for the Go compiler than parsing a slice literal. var swaggerSpec = []string{ - "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", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/api/v1/api.yaml b/api/v1/api.yaml index da723fd09a..bf87f2a204 100644 --- a/api/v1/api.yaml +++ b/api/v1/api.yaml @@ -976,9 +976,25 @@ paths: x-go-type-skip-optional-pointer: true items: type: string + issues: + type: array + description: "Structured errors, warnings and notices of the spec, with step and location data when known" + x-go-type-skip-optional-pointer: true + items: + $ref: "#/components/schemas/ValidationIssue" + valueReferenceNotices: + type: array + description: "Passive value-reference notices produced while loading this spec. These notices are not persisted." + x-go-type-skip-optional-pointer: true + items: + $ref: "#/components/schemas/ValueReferenceNotice" + referenceScope: + $ref: "#/components/schemas/ReferenceScope" required: - valid - errors + - issues + - valueReferenceNotices default: description: "Generic error response" content: @@ -1130,13 +1146,27 @@ paths: allOf: - $ref: "#/components/schemas/DAGRunId" - description: "Finished DAG-run of the same DAG whose step outputs and work directory are carried into the skipped steps. Outputs come only from steps that succeeded there. Requires steps." + outputsFromRuns: + type: object + description: "The run that each skipped step carries its outputs from, keyed by step name or ID. A step named here takes its outputs from that run in place of outputsFromRunId; the work directory still comes only from outputsFromRunId. Each run must exist for the same DAG (a normal or an ephemeral run), be finished, and hold a node of the step that succeeded or that carries copied outputs; otherwise the request returns 400 with a message that names the step and the run. A step selected to run cannot be a key. The web UI names the run of each reused step with this field and never sends values. Requires steps." + additionalProperties: + $ref: "#/components/schemas/DAGRunId" outputs: type: object - description: "Outputs of skipped steps, keyed by step name or ID and then by output name. A declared output, or any name when the step declares none, resolves through ${steps..outputs.}; the name of the step's string-form output variable sets that variable. Takes precedence over outputs from outputsFromRunId. Requires steps." + description: "Outputs of skipped steps, keyed by step name or ID and then by output name. For clients that are not the web UI; the web UI sends no values. A declared output, or any name when the step declares none, resolves through ${steps..outputs.}; the name of the step's string-form output variable sets that variable. Takes precedence over outputs from outputsFromRunId and outputsFromRuns, and works without them: the caller can supply every value itself, and can name the run the values of each step come from in outputsSources. A reference to an output that nobody supplied stays unresolved text in the step. Requires steps." additionalProperties: type: object additionalProperties: type: string + outputsSources: + type: object + description: "Optional. The run each step of outputs was captured from, keyed by step name or ID. Dagu records it in the reusedInputs of the new run; a step of outputs without an entry has no entry there. An entry for a step not in outputs returns 400. The run is not required to exist any more. Requires steps." + additionalProperties: + $ref: "#/components/schemas/ReusedOutputSource" + ephemeral: + type: boolean + default: false + description: "If true, the run leaves no trace. It runs like any run and is readable by its ID (details, step logs, step outputs, spec, SSE) while it exists, but it is not listed anywhere by default (run lists, DAG history unless includeEphemeral is set, last run, queues, dashboard, artifacts, events and notifications), it does not count in retention, and it cannot be retried, rescheduled or edited (409). The server removes it 10 minutes after it ends and at server start. Allowed only together with steps; a request for the whole DAG with ephemeral true returns 400. The audit log keeps the request, marked ephemeral." tags: allOf: - $ref: "#/components/schemas/Tags" @@ -1158,6 +1188,20 @@ paths: type: object required: - steps + - type: object + required: + - outputsFromRuns + not: + type: object + required: + - steps + - type: object + required: + - outputsSources + not: + type: object + required: + - steps responses: "200": description: "A successful response" @@ -1387,6 +1431,13 @@ paths: parameters: - $ref: "#/components/parameters/RemoteNode" - $ref: "#/components/parameters/DAGFileName" + - name: includeEphemeral + in: query + required: false + description: "If true, the response also contains the ephemeral runs of this DAG, merged by start time with the other runs under the same limit. Each carries ephemeral true. No other list returns them." + schema: + type: boolean + default: false responses: "200": description: "A successful response" @@ -1468,6 +1519,10 @@ paths: spec: type: string description: "The DAG spec in YAML format" + sha256: + type: string + description: "Lowercase hex SHA-256 of the file bytes that spec holds" + x-go-type-skip-optional-pointer: true errors: type: array description: "List of errors in the spec" @@ -1513,6 +1568,10 @@ paths: spec: type: string description: "The new DAG spec in YAML format" + expectedSha256: + type: string + description: "Lowercase hex SHA-256 of the file that the client edited. When set and different from the current file, the server writes nothing and returns 409." + pattern: "^[a-f0-9]{64}$" required: - spec responses: @@ -1528,8 +1587,18 @@ paths: description: "List of errors in the spec" items: type: string + sha256: + type: string + description: "Lowercase hex SHA-256 of the bytes written" required: - errors + - sha256 + "409": + description: "The current file does not match expectedSha256" + content: + application/json: + schema: + $ref: "#/components/schemas/Error" default: description: "Generic error response" @@ -2197,13 +2266,27 @@ paths: allOf: - $ref: "#/components/schemas/DAGRunId" - description: "Finished DAG-run of the same DAG whose step outputs and work directory are carried into the skipped steps. Outputs come only from steps that succeeded there. Requires steps." + outputsFromRuns: + type: object + description: "The run that each skipped step carries its outputs from, keyed by step name or ID. A step named here takes its outputs from that run in place of outputsFromRunId; the work directory still comes only from outputsFromRunId. Each run must exist for the same DAG (a normal or an ephemeral run), be finished, and hold a node of the step that succeeded or that carries copied outputs; otherwise the request returns 400 with a message that names the step and the run. A step selected to run cannot be a key. The web UI names the run of each reused step with this field and never sends values. Requires steps." + additionalProperties: + $ref: "#/components/schemas/DAGRunId" outputs: type: object - description: "Outputs of skipped steps, keyed by step name or ID and then by output name. A declared output, or any name when the step declares none, resolves through ${steps..outputs.}; the name of the step's string-form output variable sets that variable. Takes precedence over outputs from outputsFromRunId. Requires steps." + description: "Outputs of skipped steps, keyed by step name or ID and then by output name. For clients that are not the web UI; the web UI sends no values. A declared output, or any name when the step declares none, resolves through ${steps..outputs.}; the name of the step's string-form output variable sets that variable. Takes precedence over outputs from outputsFromRunId and outputsFromRuns, and works without them: the caller can supply every value itself, and can name the run the values of each step come from in outputsSources. A reference to an output that nobody supplied stays unresolved text in the step. Requires steps." additionalProperties: type: object additionalProperties: type: string + outputsSources: + type: object + description: "Optional. The run each step of outputs was captured from, keyed by step name or ID. Dagu records it in the reusedInputs of the new run; a step of outputs without an entry has no entry there. An entry for a step not in outputs returns 400. The run is not required to exist any more. Requires steps." + additionalProperties: + $ref: "#/components/schemas/ReusedOutputSource" + ephemeral: + type: boolean + default: false + description: "If true, the run leaves no trace. It runs like any run and is readable by its ID (details, step logs, step outputs, spec, SSE) while it exists, but it is not listed anywhere by default (run lists, DAG history unless includeEphemeral is set, last run, queues, dashboard, artifacts, events and notifications), it does not count in retention, and it cannot be retried, rescheduled or edited (409). The server removes it 10 minutes after it ends and at server start. Allowed only together with steps; a request for the whole DAG with ephemeral true returns 400. The audit log keeps the request, marked ephemeral." tags: allOf: - $ref: "#/components/schemas/Tags" @@ -2227,6 +2310,20 @@ paths: type: object required: - steps + - type: object + required: + - outputsFromRuns + not: + type: object + required: + - steps + - type: object + required: + - outputsSources + not: + type: object + required: + - steps responses: "200": description: "Run created and started" @@ -2550,7 +2647,7 @@ paths: schema: $ref: "#/components/schemas/Error" "409": - description: "Conflict (run ID already exists)" + description: "Conflict (run ID already exists, or the historic run is ephemeral)" content: application/json: schema: @@ -2645,6 +2742,12 @@ paths: - ineligibleSteps - errors - warnings + "409": + description: "Conflict: the DAG-run is ephemeral" + content: + application/json: + schema: + $ref: "#/components/schemas/Error" default: description: "Generic error response" content: @@ -2713,6 +2816,12 @@ paths: - queued - skippedSteps - startedSteps + "409": + description: "Conflict: the DAG-run is ephemeral" + content: + application/json: + schema: + $ref: "#/components/schemas/Error" default: description: "Generic error response" content: @@ -3058,6 +3167,12 @@ paths: responses: "200": description: "A successful response" + "409": + description: "Conflict: the DAG-run is ephemeral" + content: + application/json: + schema: + $ref: "#/components/schemas/Error" default: description: "Generic error response" content: @@ -3299,6 +3414,191 @@ paths: schema: $ref: "#/components/schemas/Error" + /dag-runs/{name}/{dagRunId}/steps/{stepName}/samples: + get: + summary: "Retrieve the samples of a continuous step, in buckets" + description: "Returns the executions of a continuous step as buckets of one tier (1 second, 1 minute, 1 hour or 24 hours). The server picks the finest tier whose retention covers `from` and whose bucket count in the window is not more than `points`. The response has at most `points` buckets and does not depend on the count of executions." + operationId: "getDAGRunStepSamples" + tags: + - "dag-runs" + parameters: + - $ref: "#/components/parameters/RemoteNode" + - $ref: "#/components/parameters/DAGName" + - $ref: "#/components/parameters/DAGRunId" + - $ref: "#/components/parameters/StepName" + - name: from + in: query + description: "Start of the window (RFC 3339). Default: 24 hours before `to`." + schema: + type: string + format: date-time + - name: to + in: query + description: "End of the window (RFC 3339). Default: now." + schema: + type: string + format: date-time + - name: points + in: query + description: "Most buckets to return." + schema: + type: integer + minimum: 10 + maximum: 2000 + default: 600 + responses: + "200": + description: "A successful response" + content: + application/json: + schema: + $ref: "#/components/schemas/StepSamples" + "400": + description: "A query parameter is not valid or out of its range, or from is after to" + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "404": + description: "DAG-run or step not found, or the step is not a continuous step" + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + default: + description: "Generic error response" + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + /dag-runs/{name}/{dagRunId}/steps/{stepName}/events: + get: + summary: "Retrieve the events of a continuous step" + description: "Lists the events of a continuous step, the newest first: start, stop, failed and recovered." + operationId: "getDAGRunStepEvents" + tags: + - "dag-runs" + parameters: + - $ref: "#/components/parameters/RemoteNode" + - $ref: "#/components/parameters/DAGName" + - $ref: "#/components/parameters/DAGRunId" + - $ref: "#/components/parameters/StepName" + - name: from + in: query + description: "Start of the window (RFC 3339), inclusive. Default: no bound." + schema: + type: string + format: date-time + - name: to + in: query + description: "End of the window (RFC 3339), exclusive. Default: no bound." + schema: + type: string + format: date-time + - name: limit + in: query + description: "Most events to return." + schema: + type: integer + minimum: 1 + maximum: 500 + default: 100 + - name: before + in: query + description: "Opaque cursor from `nextCursor` of a previous response." + schema: + type: string + responses: + "200": + description: "A successful response" + content: + application/json: + schema: + $ref: "#/components/schemas/StepEvents" + "400": + description: "A query parameter is not valid or out of its range, from is after to, or before is not a cursor that the server gave" + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "404": + description: "DAG-run or step not found, or the step is not a continuous step" + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + default: + description: "Generic error response" + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + /dag-runs/{name}/{dagRunId}/steps/{stepName}/availability: + get: + summary: "Retrieve the availability of a continuous step" + description: "Gives the availability of the last 24 hours, 7 days, 30 days and 365 days, and the current streak." + operationId: "getDAGRunStepAvailability" + tags: + - "dag-runs" + parameters: + - $ref: "#/components/parameters/RemoteNode" + - $ref: "#/components/parameters/DAGName" + - $ref: "#/components/parameters/DAGRunId" + - $ref: "#/components/parameters/StepName" + responses: + "200": + description: "A successful response" + content: + application/json: + schema: + $ref: "#/components/schemas/StepAvailability" + "404": + description: "DAG-run or step not found, or the step is not a continuous step" + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + default: + description: "Generic error response" + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + /dag-runs/{name}/{dagRunId}/steps/{stepName}/outputs: + get: + summary: "Retrieve outputs of a specific step in a DAG-run" + description: "Fetches the persisted outputs and the output variable of one step in a DAG-run. The stepName parameter accepts a step name or a step id. The runtime masks secret values before it persists them. Each value is cut at 65536 bytes." + operationId: "getDAGRunStepOutputs" + tags: + - "dag-runs" + parameters: + - $ref: "#/components/parameters/RemoteNode" + - $ref: "#/components/parameters/DAGName" + - $ref: "#/components/parameters/DAGRunId" + - $ref: "#/components/parameters/StepName" + responses: + "200": + description: "A successful response" + content: + application/json: + schema: + $ref: "#/components/schemas/StepOutputs" + "404": + description: "DAG-run or step not found" + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + default: + description: "Generic error response" + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /dag-runs/{name}/{dagRunId}/steps/{stepName}/log/download: get: summary: "Download log for a specific step in a DAG-run" @@ -15002,6 +15302,202 @@ components: required: - message + ValidationIssue: + type: object + description: "One structured validation result of a DAG spec." + properties: + severity: + type: string + description: "Severity of the issue." + enum: + - error + - warning + - notice + source: + type: string + description: "Stage that produced the issue." + enum: + - load + - build + - warning + - valueReference + message: + type: string + description: "Human-readable message. For errors, the same text as the matching errors entry." + stepName: + type: string + description: "Normalized name of the step, when known." + stepPath: + type: string + description: "Step key such as steps[3] or handler_on.failure, when known." + fieldPath: + type: string + description: "DAG field path such as steps[3].with.url or consts.image, when known." + line: + type: integer + description: "1-based line in the spec, when known." + column: + type: integer + description: "1-based column in the spec, when known." + reason: + type: string + description: "Machine-readable reason, for value-reference notices." + required: + - severity + - source + - message + + ReferenceScope: + type: object + description: "Values that a step of the DAG can reference." + properties: + params: + type: array + items: + $ref: "#/components/schemas/ReferenceScopeVariable" + consts: + type: array + items: + $ref: "#/components/schemas/ReferenceScopeVariable" + env: + type: array + description: "Environment variable names. Values are not included." + items: + $ref: "#/components/schemas/ReferenceScopeVariable" + secrets: + type: array + description: "Secret names. Values are not included." + items: + $ref: "#/components/schemas/ReferenceScopeVariable" + steps: + type: array + items: + $ref: "#/components/schemas/ReferenceScopeStep" + required: + - params + - consts + - env + - secrets + - steps + + ReferenceScopeVariable: + type: object + description: "A named value that a step can reference." + properties: + name: + type: string + reference: + type: string + description: "Canonical reference token, such as ${params.region}." + description: + type: string + defaultValue: + type: string + description: "Default value. Set only for params and consts." + required: + - name + - reference + + ReferenceScopeStep: + type: object + description: "A step and the outputs that other steps can reference." + properties: + name: + type: string + id: + type: string + stepPath: + type: string + description: "Step key such as steps[3] or handler_on.exit." + upstream: + type: array + description: "Names of all ancestor steps." + items: + type: string + outputs: + type: array + items: + $ref: "#/components/schemas/ReferenceScopeOutput" + outputsKnown: + type: string + description: "static when the outputs are known from the spec, runtime when only a run gives them." + enum: + - static + - runtime + required: + - name + - stepPath + - upstream + - outputs + - outputsKnown + + ReferenceScopeOutput: + type: object + description: "An output of a step that other steps can reference." + properties: + name: + type: string + reference: + type: string + description: "Reference token, such as ${steps.fetch.outputs.body} or ${RESULT} for an output variable." + source: + type: string + enum: + - declared + - stdoutField + - outputVariable + - action + type: + type: string + required: + - name + - reference + - source + + StepOutputs: + type: object + description: "Persisted outputs of one step in a DAG-run." + properties: + stepName: + type: string + stepId: + type: string + status: + $ref: "#/components/schemas/NodeStatus" + statusLabel: + $ref: "#/components/schemas/NodeStatusLabel" + outputs: + type: array + items: + $ref: "#/components/schemas/StepOutputValue" + outputVariable: + $ref: "#/components/schemas/StepOutputValue" + required: + - stepName + - status + - statusLabel + - outputs + + StepOutputValue: + type: object + description: "One output value of a step. The value is cut at 65536 bytes." + properties: + name: + type: string + value: + type: string + truncated: + type: boolean + description: "True when the value was cut." + size: + type: integer + description: "Size of the value in bytes before the cut." + required: + - name + - value + - truncated + - size + DAGEditorHints: type: object description: "Editor-only metadata used to synthesize per-document schema hints" @@ -15279,16 +15775,19 @@ components: description: "RFC 3339 timestamp of when the DAG-run was scheduled to run" startedAt: type: string - description: "RFC 3339 timestamp when the DAG-run started" + description: "RFC 3339 timestamp with millisecond precision when the DAG-run started. A run that an older version stored has second precision." finishedAt: type: string - description: "RFC 3339 timestamp when the DAG-run finished" + description: "RFC 3339 timestamp with millisecond precision when the DAG-run finished. A run that an older version stored has second precision." artifactsAvailable: type: boolean description: "Whether artifact files are available for this DAG-run" noReuse: type: boolean description: "Whether reuse of prior build materializations was disabled for this DAG-run" + ephemeral: + type: boolean + description: "True for an ephemeral run (a test run that leaves no trace). Absent or false for other runs. Only the DAG history with includeEphemeral and the run by ID return such a run." params: type: string description: "Runtime parameters passed to the DAG-run in JSON format" @@ -15332,6 +15831,47 @@ components: - artifactsAvailable - autoRetryCount + ReusedOutputSource: + type: object + description: "The run that a step's supplied outputs were captured from." + properties: + runId: + $ref: "#/components/schemas/DAGRunId" + capturedAt: + type: string + description: "RFC 3339 timestamp of when the outputs were captured. Optional." + required: + - runId + + ReusedInput: + type: object + description: "Provenance of the outputs that a skipped step of a selected-steps run carries." + properties: + step: + type: string + description: "Name of the skipped step" + runId: + $ref: "#/components/schemas/DAGRunId" + capturedAt: + type: string + description: "RFC 3339 timestamp of when the outputs were captured. Absent when unknown." + startedAt: + type: string + description: "RFC 3339 timestamp with millisecond precision of when the step started in the source run. Present for via run; absent for via cache." + finishedAt: + type: string + description: "RFC 3339 timestamp with millisecond precision of when the step finished in the source run. Present for via run; absent for via cache." + via: + type: string + enum: + - run + - cache + description: "run: the outputs were carried from the run named by outputsFromRunId, or by outputsFromRuns for this step. cache: a client that is not the web UI supplied the outputs and named the run they were captured from in outputsSources." + required: + - step + - runId + - via + DAGRunDetails: type: object description: "Detailed status of a DAG-run including sub DAG-run nodes" @@ -15410,6 +15950,11 @@ components: attemptId: type: string description: "Identifier of the attempt this status belongs to. Each retry of the DAG-run, including a resume after manual action, records a new attempt." + reusedInputs: + type: array + description: "Where the carried outputs of the skipped steps of a selected-steps run come from, one entry per skipped step, in DAG step order. Absent when the run carried no outputs." + items: + $ref: "#/components/schemas/ReusedInput" required: - rootDAGRunName - rootDAGRunId @@ -15648,10 +16193,10 @@ components: description: "Agent's justification, or why the call was rejected" startedAt: type: string - description: "RFC3339 timestamp when the step started" + description: "RFC 3339 timestamp with millisecond precision when the step started" finishedAt: type: string - description: "RFC3339 timestamp when the step finished" + description: "RFC 3339 timestamp with millisecond precision when the step finished" childDagRunId: type: string description: "Child DAG-run this action produced, for linking to its run page. Absent for steps that run no child DAG." @@ -15732,10 +16277,10 @@ components: description: "Path to the standard error log file for this step" startedAt: type: string - description: "RFC3339 timestamp when the step started" + description: "RFC 3339 timestamp with millisecond precision when the step started. A step that an older version stored has second precision." finishedAt: type: string - description: "RFC3339 timestamp when the step finished" + description: "RFC 3339 timestamp with millisecond precision when the step finished. A step that an older version stored has second precision." status: $ref: "#/components/schemas/NodeStatus" statusLabel: @@ -15746,6 +16291,9 @@ components: doneCount: type: integer description: "Number of successful completions for repeating steps" + continuous: + type: boolean + description: "True for a continuous step: a step that repeats without a limit. Its executions are recorded as samples (see the samples, events and availability endpoints)." subRuns: type: array description: "List of sub DAG-runs associated with this step" @@ -16137,10 +16685,10 @@ components: $ref: "#/components/schemas/StatusLabel" startedAt: type: string - description: "RFC 3339 timestamp when the sub DAG-run started" + description: "RFC 3339 timestamp with millisecond precision when the sub DAG-run started" finishedAt: type: string - description: "RFC 3339 timestamp when the sub DAG-run finished" + description: "RFC 3339 timestamp with millisecond precision when the sub DAG-run finished" required: - dagRunId - status @@ -16870,10 +17418,10 @@ components: description: "Error the body run reported, when it failed" startedAt: type: string - description: "RFC3339 timestamp when the body run started" + description: "RFC 3339 timestamp with millisecond precision when the body run started" finishedAt: type: string - description: "RFC3339 timestamp when the body run finished" + description: "RFC 3339 timestamp with millisecond precision when the body run finished" required: - item - index @@ -16900,8 +17448,10 @@ components: type: string startedAt: type: string + description: "RFC 3339 timestamp with millisecond precision when the body run started" finishedAt: type: string + description: "RFC 3339 timestamp with millisecond precision when the body run finished" steps: type: array description: "Body steps in execution order" @@ -16931,8 +17481,10 @@ components: type: string startedAt: type: string + description: "RFC 3339 timestamp with millisecond precision when the body step started" finishedAt: type: string + description: "RFC 3339 timestamp with millisecond precision when the body step finished" retryCount: type: integer hasStdout: @@ -16955,6 +17507,157 @@ components: - hasStdout - hasStderr + SampleBucket: + type: object + description: "The samples of one time bucket of a continuous step" + properties: + time: + type: string + format: date-time + description: "Start of the bucket, in UTC" + count: + type: integer + format: int64 + description: "Count of samples in the bucket" + failures: + type: integer + format: int64 + description: "Count of failed samples (exit code not 0)" + minMs: + type: integer + format: int64 + maxMs: + type: integer + format: int64 + p50Ms: + type: integer + format: int64 + p95Ms: + type: integer + format: int64 + required: + - time + - count + - failures + - minMs + - maxMs + - p50Ms + - p95Ms + + StepSamples: + type: object + properties: + resolutionSeconds: + type: integer + description: "Width of the tier that the buckets come from: 1, 60, 3600 or 86400" + from: + type: string + format: date-time + to: + type: string + format: date-time + buckets: + type: array + description: "Buckets in time order. A bucket with no sample is not listed." + items: + $ref: "#/components/schemas/SampleBucket" + required: + - resolutionSeconds + - from + - to + - buckets + + StepEventKind: + type: string + enum: + - started + - stopped + - failed + - recovered + + StepEvent: + type: object + properties: + time: + type: string + format: date-time + kind: + $ref: "#/components/schemas/StepEventKind" + exitCode: + type: integer + message: + type: string + description: "Last line of the standard error of a failed execution, 256 bytes at most" + failedCount: + type: integer + format: int64 + description: "For `recovered` and for a repeated `failed` event: count of failed samples of the outage" + outageSeconds: + type: integer + format: int64 + description: "For `recovered`: length of the outage" + required: + - time + - kind + + StepEvents: + type: object + properties: + events: + type: array + items: + $ref: "#/components/schemas/StepEvent" + nextCursor: + type: string + description: "Cursor for the next page. Absent when there are no more events." + required: + - events + + AvailabilityWindow: + type: object + properties: + window: + type: string + enum: ["24h", "7d", "30d", "365d"] + count: + type: integer + format: int64 + failures: + type: integer + format: int64 + availability: + type: number + format: double + description: "From 0 to 1. Absent when `count` is 0." + required: + - window + - count + - failures + + StepAvailability: + type: object + properties: + windows: + type: array + items: + $ref: "#/components/schemas/AvailabilityWindow" + streak: + type: integer + format: int64 + description: "Count of samples since the last failed sample. A lower bound once the raw tier no longer holds that sample." + lastFailureAt: + type: string + format: date-time + description: "Time of the last failed sample, or the start of its aggregate bucket once the raw tier no longer holds it. Absent when none failed." + totalCount: + type: integer + format: int64 + description: "Count of all executions" + required: + - windows + - streak + - totalCount + Log: type: object description: "Log information for the execution" diff --git a/conformance/cli/start_test.go b/conformance/cli/start_test.go index 6f4a5c3fbe..351df4779d 100644 --- a/conformance/cli/start_test.go +++ b/conformance/cli/start_test.go @@ -62,8 +62,39 @@ func TestStartOnlyKeepsReference(t *testing.T) { dagu.ExpectFileContent("consume.out", "${steps.produce.outputs.value}\nmissing\n") } -// TestStartOnlySetsOutput proves --output feeds a declared output to the -// selected step with no earlier run, through a strict step-output reference. +// TestStartOnlyOutputFrom proves --output-from feeds the declared outputs of +// one finished run to the selected step. +func TestStartOnlyOutputFrom(t *testing.T) { + t.Parallel() + + dagu := harness.NewRunner(t) + env := sharedEnv(t) + + source := dagu.RunWithEnv(env, "start", "--run-id=cli-start-output-from", "only_outputs.yaml") + source.ExpectExitCode(0) + dagu.WriteFile("consume.out", "") + + result := dagu.RunWithEnv(env, "start", "--only=consume", "--output-from=produce=cli-start-output-from", "only_outputs.yaml") + result.ExpectExitCode(0) + // Only --outputs-from carries the work directory of the source run. + dagu.ExpectFileContent("consume.out", "from-source\nmissing\n") +} + +// TestStartOnlyOutputFromMissingRun proves that a run that does not exist +// stops the command before any step runs. +func TestStartOnlyOutputFromMissingRun(t *testing.T) { + t.Parallel() + + dagu := harness.NewRunner(t) + result := dagu.RunWithEnv(sharedEnv(t), "start", "--only=consume", "--output-from=produce=cli-start-no-such-run", "only_outputs.yaml") + result.ExpectNonZeroExitCode() + result.ExpectStderrContains("step produce cannot be reused from run cli-start-no-such-run") + dagu.ExpectNoFile("consume.out") +} + +// TestStartOnlySetsOutput proves the deprecated --output still feeds a +// declared output to the selected step with no earlier run, through a strict +// step-output reference. func TestStartOnlySetsOutput(t *testing.T) { t.Parallel() diff --git a/conformance/spec013_step_run/ephemeral_test.go b/conformance/spec013_step_run/ephemeral_test.go new file mode 100644 index 0000000000..e4c0b416e1 --- /dev/null +++ b/conformance/spec013_step_run/ephemeral_test.go @@ -0,0 +1,482 @@ +// Copyright (C) 2026 Yota Hamada +// SPDX-License-Identifier: GPL-3.0-or-later + +package spec013_step_run_test + +import ( + "fmt" + "net/http" + "runtime" + "testing" + "time" + + api "github.com/dagucloud/dagu/v2/api/v1" + "github.com/dagucloud/dagu/v2/internal/service/frontend" + "github.com/dagucloud/dagu/v2/internal/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// ephemeralTTL is how long a test server keeps an ended ephemeral run. The +// product constant is ten minutes; the test server takes a short value +// through a server option that no configuration key reaches. +const ephemeralTTL = 6 * time.Second + +const stepRunDAG = `steps: + - name: build + id: build + run: 'printf "image_tag=from-run\n" >> "$DAGU_OUTPUT_FILE"' + outputs: + - name: image_tag + - name: capture + id: capture + run: "echo from-run" + output: CAPTURED + - name: consume + id: consume + depends: [build, capture] + run: 'echo "tag=${steps.build.outputs.image_tag} captured=$CAPTURED"' +` + +const timeout = 30 * time.Second + +func newServer(t *testing.T) test.Server { + t.Helper() + return test.SetupServer(t, test.WithServerOptions( + frontend.WithEphemeralRunCleanup(ephemeralTTL, 500*time.Millisecond), + )) +} + +func createDAG(t *testing.T, server test.Server, name, spec string) { + t.Helper() + server.Client().Post("/api/v1/dags", api.CreateNewDAGJSONRequestBody{ + Name: api.DAGName(name), + Spec: &spec, + }).ExpectStatus(http.StatusCreated).Send(t) +} + +func startRun(t *testing.T, server test.Server, path string, body any) string { + t.Helper() + resp := server.Client().Post(path, body).ExpectStatus(http.StatusOK).Send(t) + var result struct { + DagRunID string `json:"dagRunId"` + } + resp.Unmarshal(t, &result) + require.NotEmpty(t, result.DagRunID) + return result.DagRunID +} + +func waitForStatus(t *testing.T, server test.Server, name, runID string, want api.Status) { + t.Helper() + var last api.Status + require.EventuallyWithT(t, func(c *assert.CollectT) { + resp := server.Client().Get(fmt.Sprintf("/api/v1/dag-runs/%s/%s", name, runID)).Send(t) + if !assert.Equal(c, http.StatusOK, resp.Response.StatusCode()) { + return + } + var details api.GetDAGRunDetails200JSONResponse + resp.Unmarshal(t, &details) + last = details.DagRunDetails.Status + assert.Equal(c, want, last) + }, timeout, 200*time.Millisecond, "run %s/%s never reached %v (last %v)", name, runID, want, last) +} + +func stepLog(t *testing.T, server test.Server, name, runID, step string) string { + t.Helper() + resp := server.Client().Get(fmt.Sprintf("/api/v1/dag-runs/%s/%s/steps/%s/log", name, runID, step)). + ExpectStatus(http.StatusOK).Send(t) + var log api.Log + resp.Unmarshal(t, &log) + return log.Content +} + +func stepsOnly(outputs map[string]map[string]string, extra func(map[string]any)) map[string]any { + body := map[string]any{"steps": []string{"consume"}} + if outputs != nil { + body["outputs"] = outputs + body["outputsSources"] = explicitSources + } + if extra != nil { + extra(body) + } + return body +} + +var explicitOutputs = map[string]map[string]string{ + "build": {"image_tag": "v9.9.9"}, + "capture": {"CAPTURED": "from-the-front"}, +} + +// explicitSources names the run each step of explicitOutputs was captured +// from. The runs do not exist: a source run can be gone. +var explicitSources = map[string]map[string]string{ + "build": {"runId": "earlier-run-1", "capturedAt": "2026-10-25T08:00:00Z"}, + "capture": {"runId": "earlier-run-2"}, +} + +// An ephemeral step run executes like any run and is read by its ID, but it +// reaches no history, no DAG list entry and no retry; it disappears after the +// TTL. +func TestEphemeralStepRunLeavesNoTrace(t *testing.T) { + server := newServer(t) + const name = "ephemeral_trace" + createDAG(t, server, name, stepRunDAG) + + normalID := startRun(t, server, "/api/v1/dags/"+name+"/start", map[string]any{}) + waitForStatus(t, server, name, normalID, api.StatusSuccess) + + ephemeralID := startRun(t, server, "/api/v1/dags/"+name+"/start", stepsOnly(explicitOutputs, func(body map[string]any) { + body["ephemeral"] = true + })) + waitForStatus(t, server, name, ephemeralID, api.StatusSuccess) + + // The run reads like any run. + assert.Contains(t, stepLog(t, server, name, ephemeralID, "consume"), "tag=v9.9.9 captured=from-the-front") + server.Client().Get(fmt.Sprintf("/api/v1/dag-runs/%s/%s/spec", name, ephemeralID)). + ExpectStatus(http.StatusOK).Send(t) + + // The run is in no list. + var runs api.DAGRunsPageResponse + server.Client().Get("/api/v1/dag-runs?name="+name).ExpectStatus(http.StatusOK).Send(t).Unmarshal(t, &runs) + require.Len(t, runs.DagRuns, 1) + assert.Equal(t, normalID, string(runs.DagRuns[0].DagRunId)) + + var history api.GetDAGDAGRunHistory200JSONResponse + server.Client().Get("/api/v1/dags/"+name+"/dag-runs").ExpectStatus(http.StatusOK).Send(t).Unmarshal(t, &history) + require.Len(t, history.DagRuns, 1) + assert.Equal(t, normalID, string(history.DagRuns[0].DagRunId)) + + var dags api.ListDAGs200JSONResponse + server.Client().Get("/api/v1/dags?name="+name).ExpectStatus(http.StatusOK).Send(t).Unmarshal(t, &dags) + require.Len(t, dags.Dags, 1) + assert.Equal(t, normalID, string(dags.Dags[0].LatestDAGRun.DagRunId)) + + // The run cannot enter the history again. + server.Client().Post(fmt.Sprintf("/api/v1/dag-runs/%s/%s/retry", name, ephemeralID), map[string]any{ + "dagRunId": ephemeralID, + }).ExpectStatus(http.StatusConflict).Send(t) + server.Client().Post(fmt.Sprintf("/api/v1/dag-runs/%s/%s/reschedule", name, ephemeralID), map[string]any{}). + ExpectStatus(http.StatusConflict).Send(t) + + // The run goes away once it has ended for the TTL; the normal run stays. + require.EventuallyWithT(t, func(c *assert.CollectT) { + resp := server.Client().Get(fmt.Sprintf("/api/v1/dag-runs/%s/%s", name, ephemeralID)).Send(t) + assert.Equal(c, http.StatusNotFound, resp.Response.StatusCode()) + }, timeout+ephemeralTTL, 500*time.Millisecond, "ephemeral run %s was not removed", ephemeralID) + server.Client().Get(fmt.Sprintf("/api/v1/dag-runs/%s/%s", name, normalID)). + ExpectStatus(http.StatusOK).Send(t) +} + +// Outputs supplied in the request feed the selected step, with no source run, +// for a saved DAG and for a spec text, whether the run is ephemeral or not. +func TestOutputsFeedSelectedStep(t *testing.T) { + server := newServer(t) + const name = "step_run_outputs" + createDAG(t, server, name, stepRunDAG) + + for _, ephemeral := range []bool{false, true} { + t.Run(fmt.Sprintf("saved/ephemeral=%v", ephemeral), func(t *testing.T) { + runID := startRun(t, server, "/api/v1/dags/"+name+"/start", stepsOnly(explicitOutputs, func(body map[string]any) { + body["ephemeral"] = ephemeral + })) + waitForStatus(t, server, name, runID, api.StatusSuccess) + assert.Contains(t, stepLog(t, server, name, runID, "consume"), "tag=v9.9.9 captured=from-the-front") + }) + t.Run(fmt.Sprintf("spec/ephemeral=%v", ephemeral), func(t *testing.T) { + specName := fmt.Sprintf("inline_outputs_%v", ephemeral) + runID := startRun(t, server, "/api/v1/dag-runs", stepsOnly(explicitOutputs, func(body map[string]any) { + body["spec"] = stepRunDAG + body["name"] = specName + body["ephemeral"] = ephemeral + })) + waitForStatus(t, server, specName, runID, api.StatusSuccess) + assert.Contains(t, stepLog(t, server, specName, runID, "consume"), "tag=v9.9.9 captured=from-the-front") + }) + } +} + +// A value that nobody supplied is not an error of the request. The reference +// stays literal text (Spec 007), so the selected step runs with that text and +// fails when its shell rejects it. +func TestUnsuppliedOutputLeavesReferenceLiteral(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("relies on sh rejecting the unresolved reference") + } + server := newServer(t) + const name = "step_run_missing_output" + createDAG(t, server, name, `steps: + - name: build + id: build + run: "true" + - name: consume + id: consume + depends: [build] + run: 'echo "tag=${steps.build.outputs.image_tag}"' +`) + + runID := startRun(t, server, "/api/v1/dags/"+name+"/start", stepsOnly(nil, nil)) + waitForStatus(t, server, name, runID, api.StatusFailed) +} + +// A request for the whole DAG cannot be ephemeral. +func TestEphemeralNeedsSteps(t *testing.T) { + server := newServer(t) + const name = "ephemeral_needs_steps" + createDAG(t, server, name, stepRunDAG) + + for _, tc := range []struct { + path string + body map[string]any + }{ + {"/api/v1/dags/" + name + "/start", map[string]any{"ephemeral": true}}, + {"/api/v1/dag-runs", map[string]any{"spec": stepRunDAG, "name": "inline_whole", "ephemeral": true}}, + } { + resp := server.Client().Post(tc.path, tc.body).ExpectStatus(http.StatusBadRequest).Send(t) + assert.Contains(t, resp.Body, "ephemeral requires steps") + } + + // An explicit false is the default and runs the whole DAG. + runID := startRun(t, server, "/api/v1/dags/"+name+"/start", map[string]any{"ephemeral": false}) + waitForStatus(t, server, name, runID, api.StatusSuccess) +} + +// outputsSources is optional: a step in outputs without an entry runs with its +// values and has no reused input. A source must be for a step in outputs. A +// source run that does not exist is accepted. +func TestOutputsSources(t *testing.T) { + server := newServer(t) + const name = "outputs_sources_rules" + createDAG(t, server, name, stepRunDAG) + path := "/api/v1/dags/" + name + "/start" + + post := func(body map[string]any) string { + return server.Client().Post(path, body).ExpectStatus(http.StatusBadRequest).Send(t).Body + } + + noSources := startRun(t, server, path, map[string]any{ + "steps": []string{"consume"}, + "outputs": explicitOutputs, + }) + waitForStatus(t, server, name, noSources, api.StatusSuccess) + assert.Contains(t, stepLog(t, server, name, noSources, "consume"), "tag=v9.9.9 captured=from-the-front") + assert.Nil(t, runDetails(t, server, name, noSources).ReusedInputs) + + // Only the step that names its run has an entry. + oneSource := startRun(t, server, path, map[string]any{ + "steps": []string{"consume"}, + "outputs": explicitOutputs, + "outputsSources": map[string]any{"build": explicitSources["build"]}, + }) + waitForStatus(t, server, name, oneSource, api.StatusSuccess) + inputs := *runDetails(t, server, name, oneSource).ReusedInputs + require.Len(t, inputs, 1) + assert.Equal(t, "build", inputs[0].Step) + assert.Equal(t, api.ReusedInputViaCache, inputs[0].Via) + + body := post(map[string]any{ + "steps": []string{"consume"}, + "outputs": map[string]map[string]string{"build": {"image_tag": "v1"}}, + "outputsSources": explicitSources, + }) + assert.Contains(t, body, "outputsSources names step capture, which is not in outputs") + + body = post(map[string]any{ + "steps": []string{"consume"}, + "outputsSources": explicitSources, + }) + assert.Contains(t, body, "outputsSources names step") + + // The inline-spec endpoint applies the same rule. + resp := server.Client().Post("/api/v1/dag-runs", map[string]any{ + "spec": stepRunDAG, + "name": "inline_source_only", + "steps": []string{"consume"}, + "outputsSources": explicitSources, + }).ExpectStatus(http.StatusBadRequest).Send(t) + assert.Contains(t, resp.Body, "outputsSources names step") + + // An invalid run ID or time is a 400 too. + post(map[string]any{ + "steps": []string{"consume"}, + "outputs": map[string]map[string]string{"build": {"image_tag": "v1"}}, + "outputsSources": map[string]any{ + "build": map[string]string{"runId": "not/a/run id"}, + }, + }) + post(map[string]any{ + "steps": []string{"consume"}, + "outputs": map[string]map[string]string{"build": {"image_tag": "v1"}}, + "outputsSources": map[string]any{ + "build": map[string]string{"runId": "run-1", "capturedAt": "yesterday"}, + }, + }) + + // Sources for every step of outputs make a valid request. + runID := startRun(t, server, path, stepsOnly(explicitOutputs, nil)) + waitForStatus(t, server, name, runID, api.StatusSuccess) +} + +func runDetails(t *testing.T, server test.Server, name, runID string) api.DAGRunDetails { + t.Helper() + var details api.GetDAGRunDetails200JSONResponse + server.Client().Get(fmt.Sprintf("/api/v1/dag-runs/%s/%s", name, runID)). + ExpectStatus(http.StatusOK).Send(t).Unmarshal(t, &details) + return details.DagRunDetails +} + +// A run seeded from outputsFromRunId records the source run for each step +// that carried outputs. +func TestReusedInputsFromRun(t *testing.T) { + server := newServer(t) + const name = "reused_from_run" + createDAG(t, server, name, stepRunDAG) + + sourceID := startRun(t, server, "/api/v1/dags/"+name+"/start", map[string]any{}) + waitForStatus(t, server, name, sourceID, api.StatusSuccess) + + runID := startRun(t, server, "/api/v1/dags/"+name+"/start", map[string]any{ + "steps": []string{"consume"}, + "outputsFromRunId": sourceID, + }) + waitForStatus(t, server, name, runID, api.StatusSuccess) + + details := runDetails(t, server, name, runID) + require.NotNil(t, details.ReusedInputs) + inputs := *details.ReusedInputs + require.Len(t, inputs, 2) + for i, step := range []string{"build", "capture"} { + assert.Equal(t, step, inputs[i].Step) + assert.Equal(t, sourceID, string(inputs[i].RunId)) + assert.Equal(t, api.ReusedInputViaRun, inputs[i].Via) + require.NotNil(t, inputs[i].CapturedAt, step) + _, err := time.Parse(time.RFC3339, *inputs[i].CapturedAt) + assert.NoError(t, err, step) + } + assert.Nil(t, details.Ephemeral) + + // A normal run has no record. + assert.Nil(t, runDetails(t, server, name, sourceID).ReusedInputs) +} + +// Only a skipped step that a selected step depends on carries outputs: the +// steps that nothing selected reads have no entry, whatever the source run has. +func TestReusedInputsOnlyUpstream(t *testing.T) { + server := newServer(t) + const name = "reused_only_upstream" + createDAG(t, server, name, stepRunDAG) + + sourceID := startRun(t, server, "/api/v1/dags/"+name+"/start", map[string]any{}) + waitForStatus(t, server, name, sourceID, api.StatusSuccess) + + // build depends on nothing, so capture and consume carry nothing. + runID := startRun(t, server, "/api/v1/dags/"+name+"/start", map[string]any{ + "steps": []string{"build"}, + "outputsFromRunId": sourceID, + }) + waitForStatus(t, server, name, runID, api.StatusSuccess) + details := runDetails(t, server, name, runID) + assert.Nil(t, details.ReusedInputs) + for _, node := range details.Nodes { + if node.Step.Name == "capture" || node.Step.Name == "consume" { + assert.Equal(t, api.NodeStatusSkipped, node.Status, node.Step.Name) + assert.Empty(t, node.Stdout, node.Step.Name) + } + } +} + +// A run seeded from supplied outputs records the run each step names, and a +// supplied step wins over outputsFromRunId. +func TestReusedInputsFromCache(t *testing.T) { + server := newServer(t) + const name = "reused_from_cache" + createDAG(t, server, name, stepRunDAG) + + sourceID := startRun(t, server, "/api/v1/dags/"+name+"/start", map[string]any{}) + waitForStatus(t, server, name, sourceID, api.StatusSuccess) + + runID := startRun(t, server, "/api/v1/dags/"+name+"/start", stepsOnly(explicitOutputs, nil)) + waitForStatus(t, server, name, runID, api.StatusSuccess) + inputs := *runDetails(t, server, name, runID).ReusedInputs + require.Len(t, inputs, 2) + assert.Equal(t, "build", inputs[0].Step) + assert.Equal(t, "earlier-run-1", string(inputs[0].RunId)) + assert.Equal(t, api.ReusedInputViaCache, inputs[0].Via) + require.NotNil(t, inputs[0].CapturedAt) + assert.Equal(t, "2026-10-25T08:00:00Z", *inputs[0].CapturedAt) + assert.Equal(t, "capture", inputs[1].Step) + assert.Equal(t, "earlier-run-2", string(inputs[1].RunId)) + assert.Nil(t, inputs[1].CapturedAt) + + // Only build is supplied; capture still comes from the source run. + mixedID := startRun(t, server, "/api/v1/dags/"+name+"/start", map[string]any{ + "steps": []string{"consume"}, + "outputsFromRunId": sourceID, + "outputs": map[string]map[string]string{"build": {"image_tag": "v1.2.3"}}, + "outputsSources": map[string]any{"build": explicitSources["build"]}, + }) + waitForStatus(t, server, name, mixedID, api.StatusSuccess) + inputs = *runDetails(t, server, name, mixedID).ReusedInputs + require.Len(t, inputs, 2) + assert.Equal(t, api.ReusedInputViaCache, inputs[0].Via) + assert.Equal(t, "earlier-run-1", string(inputs[0].RunId)) + assert.Equal(t, api.ReusedInputViaRun, inputs[1].Via) + assert.Equal(t, sourceID, string(inputs[1].RunId)) +} + +// An ephemeral run says so in its details, appears in the DAG history only +// when the request opts in, and is still removed after the TTL. +func TestEphemeralRunInHistory(t *testing.T) { + server := newServer(t) + const name = "ephemeral_history" + createDAG(t, server, name, stepRunDAG) + + normalID := startRun(t, server, "/api/v1/dags/"+name+"/start", map[string]any{}) + waitForStatus(t, server, name, normalID, api.StatusSuccess) + ephemeralID := startRun(t, server, "/api/v1/dags/"+name+"/start", stepsOnly(explicitOutputs, func(body map[string]any) { + body["ephemeral"] = true + })) + waitForStatus(t, server, name, ephemeralID, api.StatusSuccess) + + details := runDetails(t, server, name, ephemeralID) + require.NotNil(t, details.Ephemeral) + assert.True(t, *details.Ephemeral) + require.NotNil(t, details.ReusedInputs) + + history := func(query string) api.GetDAGDAGRunHistory200JSONResponse { + var response api.GetDAGDAGRunHistory200JSONResponse + server.Client().Get("/api/v1/dags/"+name+"/dag-runs"+query).ExpectStatus(http.StatusOK).Send(t).Unmarshal(t, &response) + return response + } + + // Without the option, and with it set to false, only the normal run shows. + for _, query := range []string{"", "?includeEphemeral=false"} { + got := history(query) + require.Len(t, got.DagRuns, 1, query) + assert.Equal(t, normalID, string(got.DagRuns[0].DagRunId), query) + assert.Nil(t, got.DagRuns[0].Ephemeral, query) + } + + // With the option, the ephemeral run comes first: it started last. + got := history("?includeEphemeral=true") + require.Len(t, got.DagRuns, 2) + assert.Equal(t, ephemeralID, string(got.DagRuns[0].DagRunId)) + require.NotNil(t, got.DagRuns[0].Ephemeral) + assert.True(t, *got.DagRuns[0].Ephemeral) + assert.Equal(t, normalID, string(got.DagRuns[1].DagRunId)) + assert.Nil(t, got.DagRuns[1].Ephemeral) + + // The other lists never show the run, whatever the query says. + var runs api.DAGRunsPageResponse + server.Client().Get("/api/v1/dag-runs?name="+name+"&includeEphemeral=true").ExpectStatus(http.StatusOK).Send(t).Unmarshal(t, &runs) + require.Len(t, runs.DagRuns, 1) + assert.Equal(t, normalID, string(runs.DagRuns[0].DagRunId)) + + // The TTL removes the run; the history is back to the normal run. + require.EventuallyWithT(t, func(c *assert.CollectT) { + resp := server.Client().Get(fmt.Sprintf("/api/v1/dag-runs/%s/%s", name, ephemeralID)).Send(t) + assert.Equal(c, http.StatusNotFound, resp.Response.StatusCode()) + }, timeout+ephemeralTTL, 500*time.Millisecond, "ephemeral run %s was not removed", ephemeralID) + got = history("?includeEphemeral=true") + require.Len(t, got.DagRuns, 1) + assert.Equal(t, normalID, string(got.DagRuns[0].DagRunId)) +} diff --git a/conformance/spec013_step_run/from_runs_test.go b/conformance/spec013_step_run/from_runs_test.go new file mode 100644 index 0000000000..a35e48ce4a --- /dev/null +++ b/conformance/spec013_step_run/from_runs_test.go @@ -0,0 +1,208 @@ +// Copyright (C) 2026 Yota Hamada +// SPDX-License-Identifier: GPL-3.0-or-later + +package spec013_step_run_test + +import ( + "fmt" + "net/http" + "runtime" + "strings" + "testing" + "time" + + api "github.com/dagucloud/dagu/v2/api/v1" + "github.com/dagucloud/dagu/v2/internal/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// fromRunsDAG makes the output of build differ in every run: it is the run +// ID. A run started with FAIL=1 fails at build. The ID of build differs from +// its name, so that a request can name the step either way. +const fromRunsDAG = `params: + - FAIL=0 +steps: + - name: build + id: img + run: | + if [ "$FAIL" = "1" ]; then exit 1; fi + T="$DAG_RUN_ID" + echo "built $T" + printf "image_tag=%s\n" "$T" >> "$DAGU_OUTPUT_FILE" + outputs: + - name: image_tag + - name: consume + id: consume + depends: [build] + run: 'echo "tag=${steps.img.outputs.image_tag}"' +` + +// builtTag reads the value that build printed in a run. +func builtTag(t *testing.T, server test.Server, name, runID string) string { + t.Helper() + log := stepLog(t, server, name, runID, "build") + _, tag, ok := strings.Cut(strings.TrimSpace(log), "built ") + require.True(t, ok, "log of build: %q", log) + return strings.TrimSpace(tag) +} + +// A step named in outputsFromRuns carries its outputs from that run, in place +// of outputsFromRunId, and the run record says which run and when the step ran. +func TestOutputsFromRunsSeedsTheStep(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("fixtures assume a POSIX sh is available") + } + server := newServer(t) + const name = "from_runs_seed" + createDAG(t, server, name, fromRunsDAG) + path := "/api/v1/dags/" + name + "/start" + + first := startRun(t, server, path, map[string]any{}) + waitForStatus(t, server, name, first, api.StatusSuccess) + second := startRun(t, server, path, map[string]any{}) + waitForStatus(t, server, name, second, api.StatusSuccess) + firstTag, secondTag := builtTag(t, server, name, first), builtTag(t, server, name, second) + require.NotEqual(t, firstTag, secondTag) + + // The main source gives build; outputsFromRuns gives it from the other run. + runID := startRun(t, server, path, map[string]any{ + "steps": []string{"consume"}, + "outputsFromRunId": first, + "outputsFromRuns": map[string]string{"build": second}, + }) + waitForStatus(t, server, name, runID, api.StatusSuccess) + assert.Contains(t, stepLog(t, server, name, runID, "consume"), "tag="+secondTag) + + details := runDetails(t, server, name, runID) + require.NotNil(t, details.ReusedInputs) + inputs := *details.ReusedInputs + require.Len(t, inputs, 1) + assert.Equal(t, "build", inputs[0].Step) + assert.Equal(t, second, string(inputs[0].RunId)) + assert.Equal(t, api.ReusedInputViaRun, inputs[0].Via) + + // The times are the ones that build had in the source run. + source := runDetails(t, server, name, second) + var node *api.Node + for i := range source.Nodes { + if source.Nodes[i].Step.Name == "build" { + node = &source.Nodes[i] + } + } + require.NotNil(t, node) + require.NotNil(t, inputs[0].StartedAt) + require.NotNil(t, inputs[0].FinishedAt) + assert.Equal(t, node.StartedAt, *inputs[0].StartedAt) + assert.Equal(t, node.FinishedAt, *inputs[0].FinishedAt) + start, err := time.Parse(time.RFC3339, *inputs[0].StartedAt) + require.NoError(t, err) + finish, err := time.Parse(time.RFC3339, *inputs[0].FinishedAt) + require.NoError(t, err) + assert.False(t, finish.Before(start)) + require.NotNil(t, inputs[0].CapturedAt) + assert.Equal(t, *inputs[0].FinishedAt, *inputs[0].CapturedAt) + + // Without outputsFromRunId, the step still takes its outputs from its run. + alone := startRun(t, server, path, map[string]any{ + "steps": []string{"consume"}, + "outputsFromRuns": map[string]string{"build": first}, + }) + waitForStatus(t, server, name, alone, api.StatusSuccess) + assert.Contains(t, stepLog(t, server, name, alone, "consume"), "tag="+firstTag) + + // A step given by its ID names the same step. + byID := startRun(t, server, path, map[string]any{ + "steps": []string{"consume"}, + "outputsFromRuns": map[string]string{"img": second}, + }) + waitForStatus(t, server, name, byID, api.StatusSuccess) + assert.Contains(t, stepLog(t, server, name, byID, "consume"), "tag="+secondTag) + byIDInputs := *runDetails(t, server, name, byID).ReusedInputs + require.Len(t, byIDInputs, 1) + assert.Equal(t, "build", byIDInputs[0].Step) + + // A test run can be the source while it exists, and a run that reused a step + // of another run records that run. + ephemeral := startRun(t, server, path, map[string]any{ + "steps": []string{"consume"}, + "ephemeral": true, + "outputsFromRuns": map[string]string{"build": first}, + }) + waitForStatus(t, server, name, ephemeral, api.StatusSuccess) + chained := startRun(t, server, path, map[string]any{ + "steps": []string{"consume"}, + "outputsFromRuns": map[string]string{"build": ephemeral}, + }) + waitForStatus(t, server, name, chained, api.StatusSuccess) + assert.Contains(t, stepLog(t, server, name, chained, "consume"), "tag="+firstTag) + chainedInputs := *runDetails(t, server, name, chained).ReusedInputs + assert.Equal(t, ephemeral, string(chainedInputs[0].RunId)) +} + +// A run of a step that cannot give it is a bad request that names the step and +// the run: the run is gone, belongs to another DAG, or the step failed there. +func TestOutputsFromRunsRejects(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("fixtures assume a POSIX sh is available") + } + server := newServer(t) + const name = "from_runs_rules" + createDAG(t, server, name, fromRunsDAG) + const other = "from_runs_other" + createDAG(t, server, other, fromRunsDAG) + path := "/api/v1/dags/" + name + "/start" + + good := startRun(t, server, path, map[string]any{}) + waitForStatus(t, server, name, good, api.StatusSuccess) + failed := startRun(t, server, path, map[string]any{"params": "FAIL=1"}) + waitForStatus(t, server, name, failed, api.StatusFailed) + foreign := startRun(t, server, "/api/v1/dags/"+other+"/start", map[string]any{}) + waitForStatus(t, server, other, foreign, api.StatusSuccess) + + reject := func(from map[string]string) string { + return server.Client().Post(path, map[string]any{ + "steps": []string{"consume"}, + "outputsFromRuns": from, + }).ExpectStatus(http.StatusBadRequest).Send(t).Body + } + + assert.Contains(t, reject(map[string]string{"build": "gone-run"}), + "step build cannot be reused from run gone-run: the run does not exist") + assert.Contains(t, reject(map[string]string{"build": foreign}), + fmt.Sprintf("step build cannot be reused from run %s: the run does not exist", foreign)) + assert.Contains(t, reject(map[string]string{"build": failed}), + fmt.Sprintf("step build cannot be reused from run %s: the step did not succeed there", failed)) + assert.Contains(t, reject(map[string]string{"missing": good}), "unknown step") + assert.Contains(t, reject(map[string]string{"build": "bad/id"}), + "dag-run ID must only contain alphanumeric characters, dashes, and underscores") + + // A run that selected build records consume as skipped with nothing to + // carry, so it cannot give consume. + partial := startRun(t, server, path, map[string]any{"steps": []string{"build"}}) + waitForStatus(t, server, name, partial, api.StatusSuccess) + body := server.Client().Post(path, map[string]any{ + "steps": []string{"build"}, + "outputsFromRuns": map[string]string{"consume": partial}, + }).ExpectStatus(http.StatusBadRequest).Send(t).Body + assert.Contains(t, body, + fmt.Sprintf("step consume cannot be reused from run %s: the step did not succeed there", partial)) + + // A step that is selected to run cannot be a key. + body = server.Client().Post(path, map[string]any{ + "steps": []string{"build"}, + "outputsFromRuns": map[string]string{"build": good}, + }).ExpectStatus(http.StatusBadRequest).Send(t).Body + assert.Contains(t, body, "which is selected to run") + + // outputsFromRuns needs steps, on both endpoints. + body = server.Client().Post(path, map[string]any{ + "outputsFromRuns": map[string]string{"build": good}, + }).ExpectStatus(http.StatusBadRequest).Send(t).Body + assert.Contains(t, body, "outputsFromRuns") + server.Client().Post("/api/v1/dag-runs", map[string]any{ + "spec": fromRunsDAG, + "name": "from_runs_inline", + "outputsFromRuns": map[string]string{"build": good}, + }).ExpectStatus(http.StatusBadRequest).Send(t) +} diff --git a/conformance/spec013_step_run/handlers_test.go b/conformance/spec013_step_run/handlers_test.go new file mode 100644 index 0000000000..74dde293ab --- /dev/null +++ b/conformance/spec013_step_run/handlers_test.go @@ -0,0 +1,119 @@ +// Copyright (C) 2026 Yota Hamada +// SPDX-License-Identifier: GPL-3.0-or-later + +package spec013_step_run_test + +import ( + "fmt" + "net/http" + "os" + "path/filepath" + "testing" + "time" + + api "github.com/dagucloud/dagu/v2/api/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// handlersDAG is a chain a > b > c with handlers that leave marker files in +// the directory given by the MARKERS constant. The step c is the end step. +func handlersDAG(markers string, failAt string) string { + return fmt.Sprintf(`steps: + - name: a + run: "true" + - name: b + depends: [a] + run: %q + - name: c + depends: [b] + run: "true" +handler_on: + init: + run: 'touch %[2]s/init' + success: + run: 'touch %[2]s/success' + failure: + run: 'touch %[2]s/failure' + exit: + run: 'touch %[2]s/exit' +`, failAt, markers) +} + +func markerNames(t *testing.T, dir string) []string { + t.Helper() + entries, err := os.ReadDir(dir) + require.NoError(t, err) + names := make([]string, 0, len(entries)) + for _, entry := range entries { + names = append(names, entry.Name()) + } + return names +} + +// A selected-steps run calls the handlers of the DAG that run after the steps +// only when the selection has a graph end step. A handler that is not called +// leaves no node in the run status. The init handler runs as for any run. +func TestSelectedStepsCallHandlersOnlyForEndStep(t *testing.T) { + server := newServer(t) + + tests := []struct { + name string + steps []string + failAt string + wantRun api.Status + wantFiles []string + wantNodes bool + }{ + {name: "interior", steps: []string{"b"}, failAt: "true", wantRun: api.StatusSuccess, wantFiles: []string{"init"}}, + {name: "first_and_interior", steps: []string{"a", "b"}, failAt: "true", wantRun: api.StatusSuccess, wantFiles: []string{"init"}}, + {name: "interior_failure", steps: []string{"b"}, failAt: "exit 1", wantRun: api.StatusFailed, wantFiles: []string{"init"}}, + {name: "end", steps: []string{"c"}, failAt: "true", wantRun: api.StatusSuccess, wantFiles: []string{"exit", "init", "success"}, wantNodes: true}, + {name: "interior_and_end", steps: []string{"b", "c"}, failAt: "true", wantRun: api.StatusSuccess, wantFiles: []string{"exit", "init", "success"}, wantNodes: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + markers := t.TempDir() + dagName := "handlers_" + tt.name + createDAG(t, server, dagName, handlersDAG(markers, tt.failAt)) + + runID := startRun(t, server, "/api/v1/dags/"+dagName+"/start", map[string]any{"steps": tt.steps}) + waitForStatus(t, server, dagName, runID, tt.wantRun) + + // The final status is written after the handlers, so the markers + // are complete once the run has ended. + require.EventuallyWithT(t, func(c *assert.CollectT) { + assert.Equal(c, tt.wantFiles, markerNames(t, markers)) + }, timeout, 200*time.Millisecond) + time.Sleep(500 * time.Millisecond) + assert.Equal(t, tt.wantFiles, markerNames(t, markers)) + + var details api.GetDAGRunDetails200JSONResponse + server.Client().Get(fmt.Sprintf("/api/v1/dag-runs/%s/%s", dagName, runID)). + ExpectStatus(http.StatusOK).Send(t).Unmarshal(t, &details) + if tt.wantNodes { + assert.NotNil(t, details.DagRunDetails.OnSuccess) + assert.NotNil(t, details.DagRunDetails.OnExit) + } else { + assert.Nil(t, details.DagRunDetails.OnSuccess) + assert.Nil(t, details.DagRunDetails.OnFailure) + assert.Nil(t, details.DagRunDetails.OnExit) + } + assert.FileExists(t, filepath.Join(markers, "init")) + }) + } +} + +// A run of the whole DAG calls the handlers as before. +func TestFullRunCallsHandlers(t *testing.T) { + server := newServer(t) + markers := t.TempDir() + const dagName = "handlers_full" + createDAG(t, server, dagName, handlersDAG(markers, "true")) + + runID := startRun(t, server, "/api/v1/dags/"+dagName+"/start", map[string]any{}) + waitForStatus(t, server, dagName, runID, api.StatusSuccess) + require.EventuallyWithT(t, func(c *assert.CollectT) { + assert.Equal(c, []string{"exit", "init", "success"}, markerNames(t, markers)) + }, timeout, 200*time.Millisecond) +} diff --git a/conformance/spec021_mcp_read_tool/read_run_test.go b/conformance/spec021_mcp_read_tool/read_run_test.go index 29ed2878c0..80331119f3 100644 --- a/conformance/spec021_mcp_read_tool/read_run_test.go +++ b/conformance/spec021_mcp_read_tool/read_run_test.go @@ -4,7 +4,9 @@ package spec021_mcp_read_tool_test import ( + "regexp" "testing" + "time" api "github.com/dagucloud/dagu/v2/api/v1" "github.com/dagucloud/dagu/v2/conformance/mcptest" @@ -48,6 +50,38 @@ func TestReadRunTargets(t *testing.T) { }) } +// Spec 021: a run and its steps carry RFC 3339 times with millisecond +// precision, and a very fast step never finishes before it starts. +func TestReadRunTimesHaveMilliseconds(t *testing.T) { + fixture := newReadFixture(t) + result := callRead(t, fixture.session, map[string]any{ + "target": "run", + "name": fixture.dagName, + "dagRunId": fixture.dagRunID, + }) + output := requireReadSuccess(t, result, "run", runURI(fixture.dagName, fixture.dagRunID), "dag_run", "application/json") + data := requireData(t, output) + + withMillis := regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}(Z|[+-]\d{2}:\d{2})$`) + requirePair := func(holder map[string]any) { + t.Helper() + startedAt := requireString(t, holder, "startedAt") + finishedAt := requireString(t, holder, "finishedAt") + require.Regexp(t, withMillis, startedAt) + require.Regexp(t, withMillis, finishedAt) + started, err := time.Parse(time.RFC3339, startedAt) + require.NoError(t, err) + finished, err := time.Parse(time.RFC3339, finishedAt) + require.NoError(t, err) + require.False(t, finished.Before(started), "%s is before %s", finishedAt, startedAt) + } + + requirePair(data) + steps, ok := data["steps"].([]any) + require.True(t, ok) + requirePair(requireItem(t, steps, "name", "main")) +} + func TestReadStepLogQueries(t *testing.T) { fixture := newReadFixture(t) diff --git a/conformance/spec083_continuous_runs/api_test.go b/conformance/spec083_continuous_runs/api_test.go new file mode 100644 index 0000000000..56e63292b6 --- /dev/null +++ b/conformance/spec083_continuous_runs/api_test.go @@ -0,0 +1,281 @@ +// Copyright (C) 2026 Yota Hamada +// SPDX-License-Identifier: GPL-3.0-or-later + +package spec083_continuous_runs_test + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "testing" + "time" + + api "github.com/dagucloud/dagu/v2/api/v1" + "github.com/dagucloud/dagu/v2/internal/ir" + "github.com/dagucloud/dagu/v2/internal/samples" + "github.com/dagucloud/dagu/v2/internal/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const timeout = 30 * time.Second + +// beatDAG has a continuous step (a repeat mode and no limit) whose +// precondition is false, so the step never executes and the run ends. +const beatDAG = `steps: + - name: beat + run: "true" + preconditions: "test 1 = 2" + repeat_policy: + repeat: while + interval_sec: 1 +` + +const plainDAG = `steps: + - name: once + run: "true" + - name: limited + run: "true" + repeat_policy: + repeat: while + condition: "false" + limit: 3 + interval_sec: 1 +` + +func createDAG(t *testing.T, server test.Server, name, spec string) { + t.Helper() + server.Client().Post("/api/v1/dags", api.CreateNewDAGJSONRequestBody{ + Name: api.DAGName(name), + Spec: &spec, + }).ExpectStatus(http.StatusCreated).Send(t) +} + +// finishedRun creates the DAG, runs it and waits for the end of the run. +func finishedRun(t *testing.T, server test.Server, name, spec string) string { + t.Helper() + createDAG(t, server, name, spec) + resp := server.Client().Post("/api/v1/dags/"+name+"/start", map[string]any{}).ExpectStatus(http.StatusOK).Send(t) + var started struct { + DagRunID string `json:"dagRunId"` + } + resp.Unmarshal(t, &started) + require.NotEmpty(t, started.DagRunID) + + require.Eventually(t, func() bool { + resp := server.Client().Get(fmt.Sprintf("/api/v1/dag-runs/%s/%s", name, started.DagRunID)).Send(t) + if resp.Response.StatusCode() != http.StatusOK { + return false + } + var details api.GetDAGRunDetails200JSONResponse + resp.Unmarshal(t, &details) + switch details.DagRunDetails.Status { + case api.StatusSuccess, api.StatusFailed, api.StatusAborted, api.StatusPartialSuccess, api.StatusRejected: + return true + case api.StatusNotStarted, api.StatusQueued, api.StatusRunning, api.StatusWaiting: + } + return false + }, timeout, 200*time.Millisecond) + return started.DagRunID +} + +func stepURL(name, runID, step, endpoint string, query url.Values) string { + u := fmt.Sprintf("/api/v1/dag-runs/%s/%s/steps/%s/%s", name, runID, step, endpoint) + if len(query) > 0 { + u += "?" + query.Encode() + } + return u +} + +func TestNonContinuousStepIsNotFound(t *testing.T) { + server := test.SetupServer(t) + runID := finishedRun(t, server, "plain", plainDAG) + + for _, step := range []string{"once", "limited"} { + for _, endpoint := range []string{"samples", "events", "availability"} { + resp := server.Client().Get(stepURL("plain", runID, step, endpoint, nil)). + ExpectStatus(http.StatusNotFound).Send(t) + var e api.Error + resp.Unmarshal(t, &e) + assert.Equal(t, api.ErrorCodeNotFound, e.Code) + assert.Equal(t, "step "+step+" is not a continuous step", e.Message, "%s %s", step, endpoint) + } + } +} + +func TestParamsAreValidated(t *testing.T) { + server := test.SetupServer(t) + runID := finishedRun(t, server, "beat", beatDAG) + + bad := []struct { + endpoint string + query url.Values + }{ + {"samples", url.Values{"points": {"9"}}}, + {"samples", url.Values{"points": {"2001"}}}, + {"samples", url.Values{"points": {"many"}}}, + {"samples", url.Values{"from": {"yesterday"}}}, + {"samples", url.Values{"from": {"2026-02-01T00:00:00Z"}, "to": {"2026-01-01T00:00:00Z"}}}, + {"events", url.Values{"limit": {"0"}}}, + {"events", url.Values{"limit": {"501"}}}, + {"events", url.Values{"from": {"2026-02-01T00:00:00Z"}, "to": {"2026-01-01T00:00:00Z"}}}, + {"events", url.Values{"before": {"bad"}}}, + {"events", url.Values{"before": {"e0"}}}, + {"events", url.Values{"before": {"e1"}}}, + } + for _, tt := range bad { + t.Run(tt.endpoint+"?"+tt.query.Encode(), func(t *testing.T) { + server.Client().Get(stepURL("beat", runID, "beat", tt.endpoint, tt.query)). + ExpectStatus(http.StatusBadRequest).Send(t) + }) + } +} + +// skipUntilStore skips a test that reads the store while the store is not +// merged: the reader of the contract answers samples.ErrNotImplemented. +func skipUntilStore(t *testing.T) { + t.Helper() + _, err := samples.OpenRecorder(t.Context(), t.TempDir(), samples.Options{}) + if errors.Is(err, samples.ErrNotImplemented) { + t.Skip("the samples store is not merged yet") + } +} + +func TestEmptyStore(t *testing.T) { + skipUntilStore(t) + server := test.SetupServer(t) + runID := finishedRun(t, server, "beat", beatDAG) + + var s api.StepSamples + server.Client().Get(stepURL("beat", runID, "beat", "samples", nil)).ExpectStatus(http.StatusOK).Send(t).Unmarshal(t, &s) + assert.Empty(t, s.Buckets) + + var e api.StepEvents + server.Client().Get(stepURL("beat", runID, "beat", "events", nil)).ExpectStatus(http.StatusOK).Send(t).Unmarshal(t, &e) + assert.Empty(t, e.Events) + + var a api.StepAvailability + server.Client().Get(stepURL("beat", runID, "beat", "availability", nil)).ExpectStatus(http.StatusOK).Send(t).Unmarshal(t, &a) + require.Len(t, a.Windows, 4) + assert.Equal(t, api.AvailabilityWindowWindowN24h, a.Windows[0].Window) + assert.Equal(t, api.AvailabilityWindowWindowN365d, a.Windows[3].Window) + for _, w := range a.Windows { + assert.Zero(t, w.Count) + assert.Nil(t, w.Availability) + } +} + +// A store of three years answers in bounded size and time. +func TestThreeYearsOfSamples(t *testing.T) { + skipUntilStore(t) + server := test.SetupServer(t) + runID := finishedRun(t, server, "beat", beatDAG) + + ctx := server.Context + attempt, err := server.DAGRunRepository.FindAttempt(ctx, ir.NewDAGRunRef("beat", runID)) + require.NoError(t, err) + dirOf, ok := attempt.(interface{ Dir() string }) + require.True(t, ok, "attempt has no Dir") + + // The server reads the tiers by the wall clock, so the data ends now. + now := time.Now().UTC().Truncate(time.Hour) + clock := now.AddDate(-3, 0, 0) + rec, err := samples.OpenRecorder(ctx, samples.Dir(dirOf.Dir(), "beat"), samples.Options{Now: func() time.Time { return clock }}) + require.NoError(t, err) + + // One sample each hour for three years, then one each 30 seconds for the + // last two days. Every 1000th sample fails. + var total, failures uint64 + record := func(at time.Time) { + clock = at + code := 0 + if total%1000 == 999 { + code, failures = 1, failures+1 + } + require.NoError(t, rec.Record(ctx, samples.Sample{ + Time: at, Duration: time.Duration(10+total%50) * time.Millisecond, ExitCode: code, + })) + total++ + } + denseFrom := now.Add(-48 * time.Hour) + var first time.Time + for at := now.AddDate(-3, 0, 0); at.Before(denseFrom); at = at.Add(time.Hour) { + if first.IsZero() { + first = at + } + record(at) + } + for at := denseFrom; at.Before(now); at = at.Add(30 * time.Second) { + record(at) + } + clock = now + require.NoError(t, rec.Close(ctx)) + + const points = 600 + q := url.Values{ + "from": {first.Add(-24 * time.Hour).Format(time.RFC3339)}, + "to": {now.Format(time.RFC3339)}, + "points": {fmt.Sprint(points)}, + } + started := time.Now() + resp := server.Client().Get(stepURL("beat", runID, "beat", "samples", q)).ExpectStatus(http.StatusOK).Send(t) + elapsed := time.Since(started) + var s api.StepSamples + resp.Unmarshal(t, &s) + + // The spec bound is 500 ms on its reference machine; shared CI runners, + // some with the race detector, get more room. + assert.Less(t, elapsed, 2*time.Second) + assert.LessOrEqual(t, len(s.Buckets), points) + assert.NotEmpty(t, s.Buckets) + assert.Contains(t, []int{1, 60, 3600, 86400}, s.ResolutionSeconds) + var count, failed int64 + for i, b := range s.Buckets { + count += b.Count + failed += b.Failures + assert.LessOrEqual(t, b.MinMs, b.P50Ms) + assert.LessOrEqual(t, b.P50Ms, b.P95Ms) + assert.LessOrEqual(t, b.P95Ms, b.MaxMs) + if i > 0 { + assert.True(t, s.Buckets[i-1].Time.Before(b.Time), "buckets are in time order") + } + } + // The window is wider than the points allow at the day tier: the response + // cuts it to the last `points` days, so it counts the recent samples only. + assert.Positive(t, count) + assert.LessOrEqual(t, uint64(count), total) + assert.LessOrEqual(t, uint64(failed), failures) + + // A window of one day answers from a finer tier. + q = url.Values{"from": {now.Add(-24 * time.Hour).Format(time.RFC3339)}, "to": {now.Format(time.RFC3339)}} + var day api.StepSamples + server.Client().Get(stepURL("beat", runID, "beat", "samples", q)).ExpectStatus(http.StatusOK).Send(t).Unmarshal(t, &day) + assert.LessOrEqual(t, len(day.Buckets), 600) + assert.Less(t, day.ResolutionSeconds, s.ResolutionSeconds) + + // Events: newest first, in pages. + var page api.StepEvents + server.Client().Get(stepURL("beat", runID, "beat", "events", url.Values{"limit": {"2"}})). + ExpectStatus(http.StatusOK).Send(t).Unmarshal(t, &page) + require.Len(t, page.Events, 2) + assert.False(t, page.Events[0].Time.Before(page.Events[1].Time)) + require.NotNil(t, page.NextCursor) + var next api.StepEvents + server.Client().Get(stepURL("beat", runID, "beat", "events", url.Values{"limit": {"2"}, "before": {*page.NextCursor}})). + ExpectStatus(http.StatusOK).Send(t).Unmarshal(t, &next) + require.NotEmpty(t, next.Events) + assert.False(t, next.Events[0].Time.After(page.Events[1].Time)) + + // Availability: the counts agree with the failure rate of the data. + var av api.StepAvailability + server.Client().Get(stepURL("beat", runID, "beat", "availability", nil)).ExpectStatus(http.StatusOK).Send(t).Unmarshal(t, &av) + require.Len(t, av.Windows, 4) + assert.Equal(t, int64(total), av.TotalCount) + for _, w := range av.Windows { + if w.Count > 0 { + require.NotNil(t, w.Availability) + assert.InDelta(t, float64(w.Count-w.Failures)/float64(w.Count), *w.Availability, 1e-9) + } + } +} diff --git a/conformance/spec083_continuous_runs/status_log_test.go b/conformance/spec083_continuous_runs/status_log_test.go new file mode 100644 index 0000000000..91354675f9 --- /dev/null +++ b/conformance/spec083_continuous_runs/status_log_test.go @@ -0,0 +1,84 @@ +// Copyright (C) 2026 Yota Hamada +// SPDX-License-Identifier: GPL-3.0-or-later + +package spec083_continuous_runs_test + +import ( + "io/fs" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/dagucloud/dagu/v2/conformance/harness" + "github.com/stretchr/testify/require" +) + +// findFiles returns the files under root that have the given extension or name. +func findFiles(t *testing.T, root string, match func(name string) bool) []string { + t.Helper() + var found []string + err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if !d.IsDir() && match(d.Name()) { + found = append(found, path) + } + return nil + }) + require.NoError(t, err) + return found +} + +func runBeat(t *testing.T, file string) (home string, dagu *harness.Runner) { + t.Helper() + if runtime.GOOS == "windows" { + t.Skip("fixtures use a POSIX shell") + } + home = filepath.Join(t.TempDir(), "dagu") + dagu = harness.NewRunner(t) + dagu.RunWithEnv([]string{"DAGU_HOME=" + home}, "start", file).ExpectExitCode(0) + dagu.ExpectFileContent("ticks.txt", strings.Repeat("tick\n", 30)) + return home, dagu +} + +// A step that repeats without a limit is marked continuous in its status, +// and its log keeps the output of the last execution only, because each +// execution succeeded. +func TestContinuousStepStatusAndLog(t *testing.T) { + t.Parallel() + home, _ := runBeat(t, "continuous_until_done.yaml") + + statuses := findFiles(t, home, func(n string) bool { return n == "status.jsonl" }) + require.Len(t, statuses, 1) + status, err := os.ReadFile(statuses[0]) + require.NoError(t, err) + require.Contains(t, string(status), `"continuous":true`) + require.Contains(t, string(status), `"doneCount":30`) + + logs := findFiles(t, home, func(n string) bool { return strings.HasSuffix(n, ".out") && strings.HasPrefix(n, "beat.") }) + require.Len(t, logs, 1) + out, err := os.ReadFile(logs[0]) + require.NoError(t, err) + require.Equal(t, "beat\n", string(out), "the output of the executions that succeeded does not stay") +} + +// A step with a limit keeps its status and its whole log. +func TestRepeatWithLimitKeepsLog(t *testing.T) { + t.Parallel() + home, _ := runBeat(t, "limited_until_done.yaml") + + statuses := findFiles(t, home, func(n string) bool { return n == "status.jsonl" }) + require.Len(t, statuses, 1) + status, err := os.ReadFile(statuses[0]) + require.NoError(t, err) + require.NotContains(t, string(status), `"continuous"`) + + logs := findFiles(t, home, func(n string) bool { return strings.HasSuffix(n, ".out") && strings.HasPrefix(n, "beat.") }) + require.Len(t, logs, 1) + out, err := os.ReadFile(logs[0]) + require.NoError(t, err) + require.Equal(t, strings.Repeat("beat\n", 30), string(out)) +} diff --git a/conformance/spec083_continuous_runs/testdata/beat.sh b/conformance/spec083_continuous_runs/testdata/beat.sh new file mode 100644 index 0000000000..d7e71bb0e7 --- /dev/null +++ b/conformance/spec083_continuous_runs/testdata/beat.sh @@ -0,0 +1,3 @@ +printf '%s\n' tick >> ticks.txt +echo beat +if [ "$(wc -l < ticks.txt)" -ge 30 ]; then touch done; fi diff --git a/conformance/spec083_continuous_runs/testdata/continuous_until_done.yaml b/conformance/spec083_continuous_runs/testdata/continuous_until_done.yaml new file mode 100644 index 0000000000..9a9759fdf7 --- /dev/null +++ b/conformance/spec083_continuous_runs/testdata/continuous_until_done.yaml @@ -0,0 +1,8 @@ +working_dir: . +steps: + - id: beat + run: sh beat.sh + repeat_policy: + repeat: until + condition: test -f done + interval_sec: 0 diff --git a/conformance/spec083_continuous_runs/testdata/limited_until_done.yaml b/conformance/spec083_continuous_runs/testdata/limited_until_done.yaml new file mode 100644 index 0000000000..71fd8a230a --- /dev/null +++ b/conformance/spec083_continuous_runs/testdata/limited_until_done.yaml @@ -0,0 +1,9 @@ +working_dir: . +steps: + - id: beat + run: sh beat.sh + repeat_policy: + repeat: until + condition: test -f done + interval_sec: 0 + limit: 30 diff --git a/internal/cmd/remote_commands.go b/internal/cmd/remote_commands.go index 0009193619..4ca87e2ce6 100644 --- a/internal/cmd/remote_commands.go +++ b/internal/cmd/remote_commands.go @@ -291,6 +291,10 @@ func remoteRunStart(ctx *Context, args []string) error { if len(selection.steps) > 0 { steps = &selection.steps } + var outputsFromRuns *map[string]string + if len(selection.outputFrom) > 0 { + outputsFromRuns = &selection.outputFrom + } var outputs *map[string]map[string]string if len(selection.outputs) > 0 { outputs = &selection.outputs @@ -307,6 +311,7 @@ func remoteRunStart(ctx *Context, args []string) error { NoReuse: &noReuse, Steps: steps, OutputsFromRunId: stringPtrOrNil(selection.outputsFrom), + OutputsFromRuns: outputsFromRuns, Outputs: outputs, WorkerSelector: workerSelector, }) diff --git a/internal/cmd/remote_commands_test.go b/internal/cmd/remote_commands_test.go index 891180cc57..e9491d6914 100644 --- a/internal/cmd/remote_commands_test.go +++ b/internal/cmd/remote_commands_test.go @@ -295,7 +295,8 @@ func TestRemoteStartSendsSteps(t *testing.T) { initFlags(command, startFlags...) require.NoError(t, command.Flags().Set("only", "load")) require.NoError(t, command.Flags().Set("outputs-from", "source")) - require.NoError(t, command.Flags().Set("output", "extract.rows=3")) + require.NoError(t, command.Flags().Set("output-from", "extract=run-9")) + require.NoError(t, command.Flags().Set("output", "transform.rows=3")) ctx := &Context{ Context: context.Background(), Command: command, @@ -309,8 +310,10 @@ func TestRemoteStartSendsSteps(t *testing.T) { assert.Equal(t, []string{"load"}, *body.Steps) require.NotNil(t, body.OutputsFromRunId) assert.Equal(t, "source", *body.OutputsFromRunId) + require.NotNil(t, body.OutputsFromRuns) + assert.Equal(t, map[string]string{"extract": "run-9"}, *body.OutputsFromRuns) require.NotNil(t, body.Outputs) - assert.Equal(t, map[string]map[string]string{"extract": {"rows": "3"}}, *body.Outputs) + assert.Equal(t, map[string]map[string]string{"transform": {"rows": "3"}}, *body.Outputs) } // These cases replace process stdin and must remain sequential. diff --git a/internal/cmd/start.go b/internal/cmd/start.go index 173533f29c..a2c24309f3 100644 --- a/internal/cmd/start.go +++ b/internal/cmd/start.go @@ -62,9 +62,15 @@ Flags can override default settings such as DAG-run ID, DAG name, or suppress ou Use --only to run just the named steps (by name or ID) in a new DAG-run of the current definition; every other step is recorded as skipped. Add --outputs-from -to carry the step outputs and work directory of a finished run into the skipped -steps, so the selected steps can reference them. Add --output to set an output -of a skipped step directly; it takes precedence over --outputs-from. +to carry the work directory of ONE finished run, and the outputs of the skipped +steps that the selected steps depend on, so the selected steps can reference +them. Add --output-from += to take the outputs of ONE skipped step from a finished run; +it takes precedence over --outputs-from for that step. The run must be a run of +the same DAG, and the step must have succeeded in it. A step selected by --only +cannot be named in --output-from. The run records the source run of each reused +output. The deprecated --output .= still sets an output of a +skipped step directly and takes precedence over both; use --output-from. Examples: echo '"hello world"' | dagu start --params-stdin my_dag @@ -72,7 +78,7 @@ Examples: dagu start --name my_custom_name my_dag.yaml -- P1=foo P2=bar dagu start --only build my_dag dagu start --only test --only lint --outputs-from 20260101_120000 my_dag -- ENV=dev - dagu start --only fetch --output login.token=abc123 my_dag + dagu start --only use --output-from slow=20260101_120000 --output-from middle=20260102_090000 my_dag This command parses the DAG definition, resolves parameters, and initiates the DAG-run execution. `, @@ -82,7 +88,7 @@ This command parses the DAG definition, resolves parameters, and initiates the D } // Command line flags for the start command -var startFlags = []commandLineFlag{paramsFlag, paramsStdinFlag, nameFlag, dagRunIDFlag, fromRunIDFlag, parentDAGRunFlag, rootDAGRunFlag, workerLabelFlag, labelsFlag, tagsFlag, defaultWorkingDirFlag, profileFlag, startWorkerIDFlag, attemptIDFlag, triggerTypeFlag, triggerActorFlag, scheduleTimeFlag, sourceFileFlag, noReuseFlag, onlyFlag, outputsFromFlag, outputFlag} +var startFlags = []commandLineFlag{paramsFlag, paramsStdinFlag, nameFlag, dagRunIDFlag, fromRunIDFlag, parentDAGRunFlag, rootDAGRunFlag, workerLabelFlag, labelsFlag, tagsFlag, defaultWorkingDirFlag, profileFlag, startWorkerIDFlag, attemptIDFlag, triggerTypeFlag, triggerActorFlag, scheduleTimeFlag, sourceFileFlag, noReuseFlag, onlyFlag, outputsFromFlag, outputFromFlag, outputFlag} var fromRunIDFlag = commandLineFlag{ name: "from-run-id", @@ -97,12 +103,18 @@ var onlyFlag = commandLineFlag{ var outputsFromFlag = commandLineFlag{ name: "outputs-from", - usage: "Finished dag-run ID whose step outputs and work directory feed the steps selected by --only", + usage: "Finished dag-run ID whose work directory, and the outputs of the skipped steps that --only steps depend on, feed the run", +} + +var outputFromFlag = commandLineFlag{ + name: "output-from", + usage: "Take the outputs of ONE skipped step from a finished dag-run, as =; repeatable; wins over --outputs-from for that step", + isStringArray: true, } var outputFlag = commandLineFlag{ name: "output", - usage: "Output of a step skipped by --only, as .=; repeatable", + usage: "Deprecated: use --output-from =. Output of a step skipped by --only, as .=; repeatable", isStringArray: true, } diff --git a/internal/cmd/start_steps.go b/internal/cmd/start_steps.go index 0887679b17..ba360c757c 100644 --- a/internal/cmd/start_steps.go +++ b/internal/cmd/start_steps.go @@ -18,12 +18,15 @@ import ( type selectedSteps struct { steps []string outputsFrom string + // outputFrom names, by step, the run that the step carries its outputs + // from instead of outputsFrom. + outputFrom map[string]string // outputs are outputs of skipped steps, by step and then output name. outputs map[string]map[string]string } -// selectedStepsParams reads and validates --only, --outputs-from, and -// --output. +// selectedStepsParams reads and validates --only, --outputs-from, +// --output-from, and the deprecated --output. func selectedStepsParams(ctx *Context) (selectedSteps, error) { steps, err := ctx.Command.Flags().GetStringArray(onlyFlag.name) if err != nil { @@ -33,7 +36,11 @@ func selectedStepsParams(ctx *Context) (selectedSteps, error) { if err != nil { return selectedSteps{}, fmt.Errorf("failed to get outputs-from: %w", err) } - entries, err := ctx.Command.Flags().GetStringArray(outputFlag.name) + entries, err := ctx.Command.Flags().GetStringArray(outputFromFlag.name) + if err != nil { + return selectedSteps{}, fmt.Errorf("failed to get output-from: %w", err) + } + outputEntries, err := ctx.Command.Flags().GetStringArray(outputFlag.name) if err != nil { return selectedSteps{}, fmt.Errorf("failed to get output: %w", err) } @@ -42,6 +49,9 @@ func selectedStepsParams(ctx *Context) (selectedSteps, error) { return selectedSteps{}, fmt.Errorf("--outputs-from requires --only") } if len(entries) > 0 { + return selectedSteps{}, fmt.Errorf("--output-from requires --only") + } + if len(outputEntries) > 0 { return selectedSteps{}, fmt.Errorf("--output requires --only") } return selectedSteps{}, nil @@ -54,11 +64,39 @@ func selectedStepsParams(ctx *Context) (selectedSteps, error) { return selectedSteps{}, fmt.Errorf("invalid outputs-from: %w", err) } } - outputs, err := parseStepOutputs(entries) + outputFrom, err := parseOutputFrom(entries) + if err != nil { + return selectedSteps{}, err + } + outputs, err := parseStepOutputs(outputEntries) if err != nil { return selectedSteps{}, err } - return selectedSteps{steps: steps, outputsFrom: outputsFrom, outputs: outputs}, nil + return selectedSteps{steps: steps, outputsFrom: outputsFrom, outputFrom: outputFrom, outputs: outputs}, nil +} + +// parseOutputFrom parses --output-from entries of the form =. +func parseOutputFrom(entries []string) (map[string]string, error) { + if len(entries) == 0 { + return nil, nil + } + result := make(map[string]string, len(entries)) + for _, entry := range entries { + step, runID, hasRun := strings.Cut(entry, "=") + step = strings.TrimSpace(step) + runID = strings.TrimSpace(runID) + if !hasRun || step == "" || runID == "" { + return nil, fmt.Errorf("invalid --output-from %q: expected =", entry) + } + if err := validateRunID(runID); err != nil { + return nil, fmt.Errorf("invalid --output-from %q: %w", entry, err) + } + if _, exists := result[step]; exists { + return nil, fmt.Errorf("--output-from names step %q more than once", step) + } + result[step] = runID + } + return result, nil } // parseStepOutputs parses --output entries of the form .=. @@ -107,7 +145,15 @@ func runSelectedSteps(ctx *Context, dag *ir.DAG, dagRunID, params string, opts r return fmt.Errorf("failed to read status for dag-run %s: %w", selection.outputsFrom, err) } } - nodes, err := intake.SelectedStepNodes(dag, selection.steps, source, selection.outputs) + fromRuns, err := intake.LoadRunStepSources(ctx, repo, dag.Name, selection.outputFrom, nil) + if err != nil { + return err + } + nodes, reused, err := intake.SelectedStepNodes(dag, selection.steps, source, fromRuns, selection.outputs, nil) + if err != nil { + return err + } + skipHandlers, err := intake.SelectionSkipsHandlers(dag, selection.steps) if err != nil { return err } @@ -116,6 +162,8 @@ func runSelectedSteps(ctx *Context, dag *ir.DAG, dagRunID, params string, opts r DAG: dag, DAGRunID: dagRunID, Nodes: nodes, + ReusedInputs: reused, + SkipHandlers: skipHandlers, Source: source, Params: params, TriggerType: opts.triggerType, diff --git a/internal/cmd/start_test.go b/internal/cmd/start_test.go index ed19e13f9c..30efccae7b 100644 --- a/internal/cmd/start_test.go +++ b/internal/cmd/start_test.go @@ -622,8 +622,130 @@ func TestCmdStart_Only(t *testing.T) { require.Equal(t, "RESULT=got-from-source", outputVariable(t, status.Nodes[1], "RESULT")) }) - // A supplied output stands in for the skipped producer without any - // earlier run. + // --output-from names the run that gives the outputs of one skipped step, + // and the run records that run with the times of the source step. + t.Run("OutputFrom", func(t *testing.T) { + t.Parallel() + th := test.SetupCommand(t) + dag := th.DAG(t, outputsDAG) + th.RunCommand(t, cmd.Start(), test.CmdTest{ + Args: []string{"start", "--run-id=source", dag.Location}, + }) + + th.RunCommand(t, cmd.Start(), test.CmdTest{ + Args: []string{"start", "--run-id=only", "--only=consume", "--output-from=produce=source", dag.Location}, + }) + + status := readRunStatus(t, th, dag.Name, "only") + require.Equal(t, []ir.NodeStatus{ir.NodeSkipped, ir.NodeSucceeded}, nodeStatuses(status)) + require.Equal(t, "RESULT=got-from-source", outputVariable(t, status.Nodes[1], "RESULT")) + source := readRunStatus(t, th, dag.Name, "source") + require.Len(t, status.ReusedInputs, 1) + reused := status.ReusedInputs[0] + require.Equal(t, "produce", reused.Step) + require.Equal(t, "source", reused.RunID) + require.Equal(t, ir.ReusedInputViaRun, reused.Via) + require.NotEmpty(t, reused.StartedAt) + require.Equal(t, source.Nodes[0].StartedAt, reused.StartedAt) + require.Equal(t, source.Nodes[0].FinishedAt, reused.FinishedAt) + }) + + // The run of --output-from wins over --outputs-from for its step. + t.Run("OutputFromWinsOverOutputsFrom", func(t *testing.T) { + t.Parallel() + th := test.SetupCommand(t) + dag := th.DAG(t, `steps: + - id: produce + run: echo from-${DAG_RUN_ID} + output: VALUE + - id: consume + depends: produce + run: echo got-${VALUE} + output: RESULT +`) + for _, id := range []string{"first", "second"} { + th.RunCommand(t, cmd.Start(), test.CmdTest{ + Args: []string{"start", "--run-id=" + id, dag.Location}, + }) + } + + th.RunCommand(t, cmd.Start(), test.CmdTest{ + Args: []string{"start", "--run-id=only", "--only=consume", "--outputs-from=first", "--output-from=produce=second", dag.Location}, + }) + + status := readRunStatus(t, th, dag.Name, "only") + require.Equal(t, "RESULT=got-from-second", outputVariable(t, status.Nodes[1], "RESULT")) + require.Len(t, status.ReusedInputs, 1) + require.Equal(t, "second", status.ReusedInputs[0].RunID) + }) + + t.Run("OutputFromRunOfAnotherDAG", func(t *testing.T) { + t.Parallel() + th := test.SetupCommand(t) + dag := th.DAG(t, outputsDAG) + other := th.DAG(t, "steps:\n - id: other\n run: echo other\n") + th.RunCommand(t, cmd.Start(), test.CmdTest{ + Args: []string{"start", "--run-id=elsewhere", other.Location}, + }) + + err := th.RunCommandWithError(t, cmd.Start(), test.CmdTest{ + Args: []string{"start", "--run-id=only", "--only=consume", "--output-from=produce=elsewhere", dag.Location}, + }) + require.ErrorContains(t, err, "step produce cannot be reused from run elsewhere: the run does not exist for DAG "+dag.Name) + requireNoRun(t, th, dag.Name, "only") + }) + + t.Run("OutputFromMissingRun", func(t *testing.T) { + t.Parallel() + th := test.SetupCommand(t) + dag := th.DAG(t, outputsDAG) + + err := th.RunCommandWithError(t, cmd.Start(), test.CmdTest{ + Args: []string{"start", "--run-id=only", "--only=consume", "--output-from=produce=missing", dag.Location}, + }) + require.ErrorContains(t, err, "step produce cannot be reused from run missing: the run does not exist for DAG "+dag.Name) + requireNoRun(t, th, dag.Name, "only") + }) + + t.Run("OutputFromFailedStep", func(t *testing.T) { + t.Parallel() + th := test.SetupCommand(t) + dag := th.DAG(t, `steps: + - id: produce + run: exit 1 + output: VALUE + - id: consume + depends: produce + run: echo got-${VALUE} +`) + _ = th.RunCommandWithError(t, cmd.Start(), test.CmdTest{ + Args: []string{"start", "--run-id=source", dag.Location}, + }) + + err := th.RunCommandWithError(t, cmd.Start(), test.CmdTest{ + Args: []string{"start", "--run-id=only", "--only=consume", "--output-from=produce=source", dag.Location}, + }) + require.ErrorContains(t, err, "step produce cannot be reused from run source: the step did not succeed there") + requireNoRun(t, th, dag.Name, "only") + }) + + t.Run("OutputFromSelectedStep", func(t *testing.T) { + t.Parallel() + th := test.SetupCommand(t) + dag := th.DAG(t, outputsDAG) + th.RunCommand(t, cmd.Start(), test.CmdTest{ + Args: []string{"start", "--run-id=source", dag.Location}, + }) + + err := th.RunCommandWithError(t, cmd.Start(), test.CmdTest{ + Args: []string{"start", "--run-id=only", "--only=consume", "--output-from=consume=source", dag.Location}, + }) + require.ErrorContains(t, err, `names step "consume", which is selected to run`) + requireNoRun(t, th, dag.Name, "only") + }) + + // The deprecated --output stands in for the skipped producer without any + // earlier run, and records no source run. t.Run("SetsOutput", func(t *testing.T) { t.Parallel() th := test.SetupCommand(t) @@ -636,6 +758,7 @@ func TestCmdStart_Only(t *testing.T) { status := readRunStatus(t, th, dag.Name, "only") require.Equal(t, []ir.NodeStatus{ir.NodeSkipped, ir.NodeSucceeded}, nodeStatuses(status)) require.Equal(t, "RESULT=got-given", outputVariable(t, status.Nodes[1], "RESULT")) + require.Empty(t, status.ReusedInputs) }) t.Run("OutputOverridesSource", func(t *testing.T) { @@ -692,6 +815,10 @@ func TestCmdStart_Only(t *testing.T) { {name: "NeedsOnly", args: []string{"--outputs-from=source"}, wantErr: "--outputs-from requires --only"}, {name: "FromRunID", args: []string{"--only=first", "--from-run-id=source"}, wantErr: "--only cannot be combined with --from-run-id"}, {name: "SubDAGRun", args: []string{"--only=first", "--run-id=child", "--parent=parent:run", "--root=parent:run"}, wantErr: "--only cannot be combined with --parent"}, + {name: "OutputFromNeedsOnly", args: []string{"--output-from=first=source"}, wantErr: "--output-from requires --only"}, + {name: "OutputFromWithoutRun", args: []string{"--only=third", "--output-from=first"}, wantErr: `invalid --output-from "first": expected =`}, + {name: "OutputFromWithoutStep", args: []string{"--only=third", "--output-from==source"}, wantErr: `invalid --output-from "=source": expected =`}, + {name: "OutputFromTwice", args: []string{"--only=third", "--output-from=first=a", "--output-from=first=b"}, wantErr: `--output-from names step "first" more than once`}, {name: "OutputNeedsOnly", args: []string{"--output=first.value=x"}, wantErr: "--output requires --only"}, {name: "OutputWithoutValue", args: []string{"--only=third", "--output=first.value"}, wantErr: `invalid --output "first.value": expected .=`}, {name: "OutputWithoutName", args: []string{"--only=third", "--output=first=x"}, wantErr: `invalid --output "first": expected .=`}, @@ -739,6 +866,13 @@ func readRunStatus(t *testing.T, th test.Command, dagName, dagRunID string) *ir. return status } +// requireNoRun makes sure that a rejected start left no run behind. +func requireNoRun(t *testing.T, th test.Command, dagName, dagRunID string) { + t.Helper() + _, err := th.DAGRunRepository.FindAttempt(th.Context, ir.NewDAGRunRef(dagName, dagRunID)) + require.ErrorIs(t, err, dagrun.ErrDAGRunIDNotFound) +} + func nodeStatuses(status *ir.DAGRunStatus) []ir.NodeStatus { statuses := make([]ir.NodeStatus, 0, len(status.Nodes)) for _, node := range status.Nodes { diff --git a/internal/intake/seed.go b/internal/intake/seed.go index 17b8cb0a86..250106d3b5 100644 --- a/internal/intake/seed.go +++ b/internal/intake/seed.go @@ -26,6 +26,12 @@ type SeedRequest struct { DAG *ir.DAG DAGRunID string Nodes []runtime.NodeData + // ReusedInputs records where the carried outputs of skipped nodes come from. + ReusedInputs []ir.ReusedInput + + // SkipHandlers records that the run does not call the end-of-run handlers + // of the DAG. See SelectionSkipsHandlers. + SkipHandlers bool // Source is the run whose recorded outputs the skipped nodes carry. When // set, its work directory is copied into the new run and the carried @@ -38,6 +44,9 @@ type SeedRequest struct { ProfileName string DefinitionID string NoReuse bool + // Ephemeral stores the run apart from the history and marks it so that it + // leaves no trace. + Ephemeral bool LogBaseDir string ArtifactBaseDir string @@ -53,7 +62,7 @@ func SeedRun(ctx context.Context, req SeedRequest) (dagrun.Attempt, *ir.DAGRunSt repo := req.DAGRunRepository dag := req.DAG now := time.Now() - attempt, err := repo.CreateAttempt(ctx, dag, now, req.DAGRunID, persis.DAGRunCreateAttemptOptions{}) + attempt, err := repo.CreateAttempt(ctx, dag, now, req.DAGRunID, persis.DAGRunCreateAttemptOptions{Ephemeral: req.Ephemeral}) if err != nil { return nil, nil, fmt.Errorf("failed to create seeded attempt: %w", err) } @@ -93,6 +102,9 @@ func SeedRun(ctx context.Context, req SeedRequest) (dagrun.Attempt, *ir.DAGRunSt ir.WithRuntimeProfile(req.ProfileName, "", nil), ir.WithDAGDefinitionID(req.DefinitionID), ir.WithNoReuse(req.NoReuse), + ir.WithEphemeral(req.Ephemeral), + ir.WithReusedInputs(req.ReusedInputs), + ir.WithSkipHandlers(req.SkipHandlers), ) status.Params = req.Params status.ParamsList = dag.Params diff --git a/internal/intake/steps.go b/internal/intake/steps.go index 9bfb509b5b..9d561240e6 100644 --- a/internal/intake/steps.go +++ b/internal/intake/steps.go @@ -4,7 +4,9 @@ package intake import ( + "context" "encoding/json" + "errors" "fmt" "maps" "regexp" @@ -12,7 +14,9 @@ import ( "strings" "github.com/dagucloud/dagu/v2/internal/cmn/collections" + "github.com/dagucloud/dagu/v2/internal/dagrun" "github.com/dagucloud/dagu/v2/internal/ir" + "github.com/dagucloud/dagu/v2/internal/persis" "github.com/dagucloud/dagu/v2/internal/runtime" "github.com/dagucloud/dagu/v2/internal/runtime/transform" ) @@ -20,27 +24,177 @@ import ( // outputNamePattern is the name a step output reference can address. var outputNamePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]*$`) +// OutputSource names the run that supplied outputs of a skipped step were +// captured from. The run may no longer exist. +type OutputSource struct { + RunID string + // CapturedAt is an RFC 3339 timestamp, or empty when unknown. + CapturedAt string +} + +// CheckOutputSources makes sure that every entry of sources is for a step in +// outputs. A step in outputs may have no entry. Steps are given by name or ID. +// The caller checks the run IDs. +func CheckOutputSources(dag *ir.DAG, outputs map[string]map[string]string, sources map[string]OutputSource) error { + supplied, err := stepRefsByName(dag, slices.Sorted(maps.Keys(outputs)), false) + if err != nil { + return err + } + named, err := stepRefsByName(dag, slices.Sorted(maps.Keys(sources)), true) + if err != nil { + return fmt.Errorf("outputsSources: %w", err) + } + for _, name := range slices.Sorted(maps.Keys(named)) { + if _, ok := supplied[name]; !ok { + return fmt.Errorf("outputsSources names step %s, which is not in outputs", named[name]) + } + } + return nil +} + +// RunStepSources names, by step, the finished run whose recorded outputs the +// step carries, in place of the main source run. Steps are given by name or ID. +type RunStepSources map[string]*ir.DAGRunStatus + +// RunStepSourceError reports a run named for a step that cannot be read. Its +// message names the step and the run. +type RunStepSourceError struct { + Message string +} + +func (e *RunStepSourceError) Error() string { return e.Message } + +// LoadRunStepSources reads the run that each step of refs carries its outputs +// from. refs maps a step name or ID to a run ID. A run that does not exist for +// the DAG (it was removed, or it belongs to another DAG) gives a +// *RunStepSourceError that names the step and the run. A run ID that refs +// repeats is read once. check, when set, accepts or rejects each run read; its +// error is returned as it is. +func LoadRunStepSources(ctx context.Context, repo *persis.DAGRunRepository, dagName string, refs map[string]string, check func(*ir.DAGRunStatus) error) (RunStepSources, error) { + if len(refs) == 0 { + return nil, nil + } + runs := make(map[string]*ir.DAGRunStatus) + result := make(RunStepSources, len(refs)) + for _, step := range slices.Sorted(maps.Keys(refs)) { + runID := refs[step] + run, ok := runs[runID] + if !ok { + attempt, err := repo.FindAttempt(ctx, ir.NewDAGRunRef(dagName, runID)) + if errors.Is(err, dagrun.ErrDAGRunIDNotFound) { + return nil, &RunStepSourceError{Message: fmt.Sprintf("step %s cannot be reused from run %s: the run does not exist for DAG %s", step, runID, dagName)} + } + if err != nil { + return nil, fmt.Errorf("failed to find dag-run %s: %w", runID, err) + } + run, err = attempt.ReadStatus(ctx) + if err != nil { + return nil, fmt.Errorf("failed to read status for dag-run %s: %w", runID, err) + } + if check != nil { + if err := check(run); err != nil { + return nil, err + } + } + runs[runID] = run + } + result[step] = run + } + return result, nil +} + +// CheckRunStepSources makes sure that every step of fromRuns exists, is not +// selected to run, and can be reused from its run: the run is finished and its +// node for the step succeeded, or carries outputs of an earlier run. The error +// names the step and the run. +func CheckRunStepSources(dag *ir.DAG, steps []string, fromRuns RunStepSources) error { + selected := make(map[string]struct{}, len(steps)) + for _, ref := range steps { + name, err := resolveStepName(dag, ref) + if err != nil { + return err + } + selected[name] = struct{}{} + } + named, err := stepRefsByName(dag, slices.Sorted(maps.Keys(fromRuns)), true) + if err != nil { + return fmt.Errorf("outputsFromRuns: %w", err) + } + for _, name := range slices.Sorted(maps.Keys(named)) { + ref := named[name] + run := fromRuns[ref] + if _, ok := selected[name]; ok { + return fmt.Errorf("outputsFromRuns names step %q, which is selected to run", ref) + } + if run == nil { + return fmt.Errorf("step %s cannot be reused from run: no run is given", ref) + } + if run.Status.IsActive() || run.Status == ir.NotStarted { + return fmt.Errorf("step %s cannot be reused from run %s: the run is %s, not finished", ref, run.DAGRunID, run.Status) + } + index := slices.IndexFunc(run.Nodes, func(node *ir.Node) bool { return node != nil && node.Step.Name == name }) + switch { + case index < 0: + return fmt.Errorf("step %s cannot be reused from run %s: the run has no such step", ref, run.DAGRunID) + case !carriesOutputs(run.Nodes[index]): + return fmt.Errorf("step %s cannot be reused from run %s: the step did not succeed there", ref, run.DAGRunID) + } + } + return nil +} + +// carriesOutputs reports whether a node can give its outputs to another run. +// A step skipped by retry that carried nothing has no finish time. +func carriesOutputs(node *ir.Node) bool { + return transform.Reusable(node) && (node.Status != ir.NodeSkipped || node.FinishedAt != "") +} + +// stepRefsByName maps the name of each step that refs address to the ref that +// addresses it first. With unique set, it rejects a step that two refs address. +func stepRefsByName(dag *ir.DAG, refs []string, unique bool) (map[string]string, error) { + byName := make(map[string]string, len(refs)) + for _, ref := range refs { + name, err := resolveStepName(dag, ref) + if err != nil { + return nil, err + } + if first, ok := byName[name]; ok { + if unique { + return nil, fmt.Errorf("step %q is given by both %q and %q", name, first, ref) + } + continue + } + byName[name] = ref + } + return byName, nil +} + // SelectedStepNodes returns seed nodes for a run of dag that executes only -// steps, each given by step name or ID. Every other step is recorded as -// skipped; when source is set, it carries the outputs source recorded for it. -// outputs sets outputs of skipped steps, keyed by step name or ID and then by -// output name, and takes precedence over outputs carried from source. -func SelectedStepNodes(dag *ir.DAG, steps []string, source *ir.DAGRunStatus, outputs map[string]map[string]string) ([]runtime.NodeData, error) { +// steps, each given by step name or ID, and the provenance of the outputs +// that the skipped steps carry. Every other step is recorded as skipped; when +// source is set, it carries the outputs source recorded for it. fromRuns +// gives a step another run to carry from; it takes precedence over source for +// that step, and the work directory still comes from source only. outputs sets +// outputs of skipped steps, keyed by step name or ID and then by output name, +// and takes precedence over outputs carried from a run. sources names the +// run each step of outputs was captured from; a step of outputs with no entry +// has no recorded provenance. +func SelectedStepNodes(dag *ir.DAG, steps []string, source *ir.DAGRunStatus, fromRuns RunStepSources, outputs map[string]map[string]string, sources map[string]OutputSource) ([]runtime.NodeData, []ir.ReusedInput, error) { if dag.IsAgent() { - return nil, fmt.Errorf("selecting steps is not supported for %s DAGs", ir.TypeAgent) + return nil, nil, fmt.Errorf("selecting steps is not supported for %s DAGs", ir.TypeAgent) } if len(steps) == 0 { - return nil, fmt.Errorf("at least one step is required") + return nil, nil, fmt.Errorf("at least one step is required") } if source != nil && (source.Status.IsActive() || source.Status == ir.NotStarted) { - return nil, fmt.Errorf("dag-run %s is %s; outputs are reused only from a finished run", source.DAGRunID, source.Status) + return nil, nil, fmt.Errorf("dag-run %s is %s; outputs are reused only from a finished run", source.DAGRunID, source.Status) } selected := make(map[string]struct{}, len(steps)) for _, ref := range steps { name, err := resolveStepName(dag, ref) if err != nil { - return nil, err + return nil, nil, err } selected[name] = struct{}{} } @@ -51,11 +205,189 @@ func SelectedStepNodes(dag *ir.DAG, steps []string, source *ir.DAGRunStatus, out skipped = append(skipped, step.Name) } } - nodes := transform.SeedNodes(dag, source, skipped) + if err := CheckRunStepSources(dag, steps, fromRuns); err != nil { + return nil, nil, err + } + carrying, err := carryingSteps(dag, selected, skipped, fromRuns, outputs) + if err != nil { + return nil, nil, err + } + nodes := transform.SeedNodesCarrying(dag, source, skipped, carrying) + runs, err := seedFromRuns(dag, nodes, fromRuns) + if err != nil { + return nil, nil, err + } if err := setStepOutputs(dag, nodes, selected, outputs); err != nil { + return nil, nil, err + } + carried := make([]string, 0, len(skipped)) + for _, name := range skipped { + if _, ok := carrying[name]; ok { + carried = append(carried, name) + } + } + reused, err := reusedInputs(dag, carried, source, runs, outputs, sources) + if err != nil { + return nil, nil, err + } + return nodes, reused, nil +} + +// SelectionSkipsHandlers reports whether a run of dag that executes only steps, +// each given by step name or ID, must not call the end-of-run handlers of the +// DAG. It does when no selected step is a graph end step: a step that no other +// step depends on, by depends, by an inferred dependency, or as the target of a +// router (which the DAG definition records as a dependency). +func SelectionSkipsHandlers(dag *ir.DAG, steps []string) (bool, error) { + selected := make(map[string]struct{}, len(steps)) + for _, ref := range steps { + name, err := resolveStepName(dag, ref) + if err != nil { + return false, err + } + selected[name] = struct{}{} + } + hasDependents := make(map[string]struct{}, len(dag.Steps)) + for i := range dag.Steps { + for _, dep := range dag.Steps[i].AllDepends() { + hasDependents[dep] = struct{}{} + } + } + for name := range selected { + if _, interior := hasDependents[name]; !interior { + return false, nil + } + } + return true, nil +} + +// carryingSteps returns the skipped steps that carry outputs: the steps that +// a selected step depends on, directly or not, and the steps that the caller +// names in fromRuns or outputs. The other skipped steps, such as the steps +// after the selected ones, are only skipped: nothing reads their outputs. +func carryingSteps(dag *ir.DAG, selected map[string]struct{}, skipped []string, fromRuns RunStepSources, outputs map[string]map[string]string) (map[string]struct{}, error) { + byName := make(map[string]*ir.Step, len(dag.Steps)) + for i := range dag.Steps { + byName[dag.Steps[i].Name] = &dag.Steps[i] + } + carry := make(map[string]struct{}, len(skipped)) + var visit func(name string) + visit = func(name string) { + step, ok := byName[name] + if !ok { + return + } + for _, dep := range step.AllDepends() { + if _, done := carry[dep]; done { + continue + } + if _, isSelected := selected[dep]; !isSelected { + carry[dep] = struct{}{} + } + visit(dep) + } + } + for name := range selected { + visit(name) + } + for _, refs := range []map[string]struct{}{keySet(fromRuns), keySet(outputs)} { + for ref := range refs { + name, err := resolveStepName(dag, ref) + if err != nil { + return nil, err + } + if _, isSelected := selected[name]; !isSelected { + carry[name] = struct{}{} + } + } + } + return carry, nil +} + +func keySet[V any](m map[string]V) map[string]struct{} { + set := make(map[string]struct{}, len(m)) + for key := range m { + set[key] = struct{}{} + } + return set +} + +// seedFromRuns replaces the seeded node of each step of fromRuns by the node +// that the seeding of that run gives: the same seeding as for the main source. +// It returns the run of each step by step name. +func seedFromRuns(dag *ir.DAG, nodes []runtime.NodeData, fromRuns RunStepSources) (map[string]*ir.DAGRunStatus, error) { + byName := make(map[string]*ir.DAGRunStatus, len(fromRuns)) + for ref, run := range fromRuns { + name, err := resolveStepName(dag, ref) + if err != nil { + return nil, err + } + own := transform.SeedNodes(dag, run, []string{name}) + to := slices.IndexFunc(nodes, func(node runtime.NodeData) bool { return node.Step.Name == name }) + from := slices.IndexFunc(own, func(node runtime.NodeData) bool { return node.Step.Name == name }) + nodes[to] = own[from] + byName[name] = run + } + return byName, nil +} + +// reusedInputs lists, in DAG step order, where each skipped step took its +// outputs from. A supplied entry wins over a run, as the values do, and a +// run named for the step wins over the source run. A step of outputs with no +// entry in sources has no entry, also when a run carries it. +func reusedInputs(dag *ir.DAG, skipped []string, source *ir.DAGRunStatus, runs map[string]*ir.DAGRunStatus, outputs map[string]map[string]string, sources map[string]OutputSource) ([]ir.ReusedInput, error) { + outputsByName, err := stepRefsByName(dag, slices.Sorted(maps.Keys(outputs)), false) + if err != nil { return nil, err } - return nodes, nil + cached := make(map[string]OutputSource, len(sources)) + for ref, from := range sources { + name, err := resolveStepName(dag, ref) + if err != nil { + return nil, err + } + if _, supplied := outputsByName[name]; supplied { + cached[name] = from + } + } + // reusableNode finds the node that carries a step of run. + reusableNode := func(run *ir.DAGRunStatus, name string) *ir.Node { + if run == nil { + return nil + } + for _, node := range run.Nodes { + if node != nil && node.Step.Name == name && carriesOutputs(node) { + return node + } + } + return nil + } + + var reused []ir.ReusedInput + for _, name := range skipped { + if from, ok := cached[name]; ok { + reused = append(reused, ir.ReusedInput{Step: name, RunID: from.RunID, CapturedAt: from.CapturedAt, Via: ir.ReusedInputViaCache}) + continue + } + if _, supplied := outputsByName[name]; supplied { + continue + } + run := source + if own, ok := runs[name]; ok { + run = own + } + if node := reusableNode(run, name); node != nil { + reused = append(reused, ir.ReusedInput{ + Step: name, + RunID: run.DAGRunID, + CapturedAt: node.FinishedAt, + StartedAt: node.StartedAt, + FinishedAt: node.FinishedAt, + Via: ir.ReusedInputViaRun, + }) + } + } + return reused, nil } // setStepOutputs writes caller-supplied outputs onto the skipped nodes. diff --git a/internal/intake/steps_test.go b/internal/intake/steps_test.go index 84fc88827c..55ad78ba31 100644 --- a/internal/intake/steps_test.go +++ b/internal/intake/steps_test.go @@ -81,7 +81,7 @@ func TestSelectedStepNodes(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - nodes, err := intake.SelectedStepNodes(selectedStepsDAG(tt.dagType), tt.steps, tt.source, nil) + nodes, _, err := intake.SelectedStepNodes(selectedStepsDAG(tt.dagType), tt.steps, tt.source, nil, nil, nil) if tt.wantErr != "" { require.ErrorContains(t, err, tt.wantErr) return @@ -102,7 +102,7 @@ func TestSelectedStepNodes(t *testing.T) { t.Run("ReusesOutputs", func(t *testing.T) { t.Parallel() - nodes, err := intake.SelectedStepNodes(selectedStepsDAG(""), []string{"report"}, finished, nil) + nodes, _, err := intake.SelectedStepNodes(selectedStepsDAG(""), []string{"report"}, finished, nil, nil, nil) require.NoError(t, err) require.NotNil(t, nodes[0].State.StepOutputsValue) require.JSONEq(t, output, *nodes[0].State.StepOutputsValue) @@ -209,7 +209,7 @@ func TestSelectedStepNodesOutputs(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - nodes, err := intake.SelectedStepNodes(outputsDAG(), []string{"report"}, tt.source, tt.outputs) + nodes, _, err := intake.SelectedStepNodes(outputsDAG(), []string{"report"}, tt.source, nil, tt.outputs, nil) if tt.wantErr != "" { require.ErrorContains(t, err, tt.wantErr) return @@ -249,3 +249,334 @@ func outputsDAG() *ir.DAG { }, } } + +// Every step of outputs names its run, and only those steps do. +func TestCheckOutputSources(t *testing.T) { + t.Parallel() + + values := map[string]string{"token": "x"} + from := intake.OutputSource{RunID: "run-1"} + + tests := []struct { + name string + outputs map[string]map[string]string + sources map[string]intake.OutputSource + wantErr string + }{ + {name: "None"}, + { + name: "ByNameAndID", + outputs: map[string]map[string]string{"Log in": values, "fetch": values}, + sources: map[string]intake.OutputSource{"login": from, "fetch": from}, + }, + { + name: "NoSources", + outputs: map[string]map[string]string{"login": values}, + }, + { + name: "SourceForOneStep", + outputs: map[string]map[string]string{"login": values, "fetch": values}, + sources: map[string]intake.OutputSource{"fetch": from}, + }, + { + name: "SourceWithoutOutputs", + outputs: map[string]map[string]string{"login": values}, + sources: map[string]intake.OutputSource{"login": from, "fetch": from}, + wantErr: "outputsSources names step fetch, which is not in outputs", + }, + { + name: "UnknownSourceStep", + sources: map[string]intake.OutputSource{"missing": from}, + wantErr: `outputsSources: unknown step "missing"`, + }, + { + name: "SourceGivenTwice", + outputs: map[string]map[string]string{"login": values}, + sources: map[string]intake.OutputSource{"login": from, "Log in": from}, + wantErr: `outputsSources: step "Log in" is given by both`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + err := intake.CheckOutputSources(outputsDAG(), tt.outputs, tt.sources) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + }) + } +} + +// The provenance has one entry per skipped step that carried outputs, in DAG +// step order, and a supplied entry wins over the source run. +func TestSelectedStepNodesReusedInputs(t *testing.T) { + t.Parallel() + + carried := `{"token":"old"}` + source := &ir.DAGRunStatus{ + DAGRunID: "source", + Status: ir.Succeeded, + Nodes: []*ir.Node{ + {Step: ir.Step{Name: "Log in"}, Status: ir.NodeSucceeded, StepOutputsValue: &carried, StartedAt: "2026-10-26T09:59:50Z", FinishedAt: "2026-10-26T10:00:00Z"}, + {Step: ir.Step{Name: "fetch"}, Status: ir.NodeSucceeded, StartedAt: "2026-10-26T10:00:30Z", FinishedAt: "2026-10-26T10:01:00Z"}, + {Step: ir.Step{Name: "label"}, Status: ir.NodeFailed, FinishedAt: "2026-10-26T10:02:00Z"}, + }, + } + + // Only the steps that a selected step depends on carry outputs. A step after + // the selected one is skipped and carries nothing, even when the source run + // has its outputs. + t.Run("OnlyUpstreamCarries", func(t *testing.T) { + t.Parallel() + + full := &ir.DAGRunStatus{ + DAGRunID: "source", + Status: ir.Succeeded, + Nodes: []*ir.Node{ + {Step: ir.Step{Name: "Log in"}, Status: ir.NodeSucceeded, StartedAt: "2026-10-26T09:59:50Z", FinishedAt: "2026-10-26T10:00:00Z"}, + {Step: ir.Step{Name: "fetch"}, Status: ir.NodeSucceeded, StartedAt: "2026-10-26T10:00:30Z", FinishedAt: "2026-10-26T10:01:00Z"}, + {Step: ir.Step{Name: "label"}, Status: ir.NodeSucceeded, Stdout: "label.out", StartedAt: "2026-10-26T10:01:10Z", FinishedAt: "2026-10-26T10:02:00Z"}, + {Step: ir.Step{Name: "report"}, Status: ir.NodeSucceeded, Stdout: "report.out", StartedAt: "2026-10-26T10:02:10Z", FinishedAt: "2026-10-26T10:03:00Z"}, + }, + } + + nodes, reused, err := intake.SelectedStepNodes(outputsDAG(), []string{"fetch"}, full, nil, nil, nil) + require.NoError(t, err) + require.Equal(t, []ir.ReusedInput{ + {Step: "Log in", RunID: "source", CapturedAt: "2026-10-26T10:00:00Z", StartedAt: "2026-10-26T09:59:50Z", FinishedAt: "2026-10-26T10:00:00Z", Via: ir.ReusedInputViaRun}, + }, reused) + for _, node := range nodes { + switch node.Step.Name { + case "label", "report": + require.Equal(t, ir.NodeSkipped, node.State.Status) + require.Empty(t, node.State.Stdout, node.Step.Name) + require.True(t, node.State.StartedAt.IsZero(), node.Step.Name) + } + } + }) + + t.Run("FromRun", func(t *testing.T) { + t.Parallel() + + _, reused, err := intake.SelectedStepNodes(outputsDAG(), []string{"report"}, source, nil, nil, nil) + require.NoError(t, err) + require.Equal(t, []ir.ReusedInput{ + {Step: "Log in", RunID: "source", CapturedAt: "2026-10-26T10:00:00Z", StartedAt: "2026-10-26T09:59:50Z", FinishedAt: "2026-10-26T10:00:00Z", Via: ir.ReusedInputViaRun}, + {Step: "fetch", RunID: "source", CapturedAt: "2026-10-26T10:01:00Z", StartedAt: "2026-10-26T10:00:30Z", FinishedAt: "2026-10-26T10:01:00Z", Via: ir.ReusedInputViaRun}, + }, reused) + }) + + t.Run("FromCache", func(t *testing.T) { + t.Parallel() + + outputs := map[string]map[string]string{"login": {"token": "new"}} + sources := map[string]intake.OutputSource{"login": {RunID: "gone", CapturedAt: "2026-10-25T08:00:00Z"}} + _, reused, err := intake.SelectedStepNodes(outputsDAG(), []string{"report"}, nil, nil, outputs, sources) + require.NoError(t, err) + require.Equal(t, []ir.ReusedInput{ + {Step: "Log in", RunID: "gone", CapturedAt: "2026-10-25T08:00:00Z", Via: ir.ReusedInputViaCache}, + }, reused) + }) + + t.Run("CacheWinsOverRun", func(t *testing.T) { + t.Parallel() + + outputs := map[string]map[string]string{"login": {"token": "new"}} + sources := map[string]intake.OutputSource{"Log in": {RunID: "gone"}} + _, reused, err := intake.SelectedStepNodes(outputsDAG(), []string{"report"}, source, nil, outputs, sources) + require.NoError(t, err) + require.Equal(t, []ir.ReusedInput{ + {Step: "Log in", RunID: "gone", Via: ir.ReusedInputViaCache}, + {Step: "fetch", RunID: "source", CapturedAt: "2026-10-26T10:01:00Z", StartedAt: "2026-10-26T10:00:30Z", FinishedAt: "2026-10-26T10:01:00Z", Via: ir.ReusedInputViaRun}, + }, reused) + }) + + // A supplied step without a source has no entry, also when the source run + // carries it. + t.Run("CacheWithoutSource", func(t *testing.T) { + t.Parallel() + + outputs := map[string]map[string]string{"login": {"token": "new"}} + _, reused, err := intake.SelectedStepNodes(outputsDAG(), []string{"report"}, source, nil, outputs, nil) + require.NoError(t, err) + require.Equal(t, []ir.ReusedInput{ + {Step: "fetch", RunID: "source", CapturedAt: "2026-10-26T10:01:00Z", StartedAt: "2026-10-26T10:00:30Z", FinishedAt: "2026-10-26T10:01:00Z", Via: ir.ReusedInputViaRun}, + }, reused) + }) + + t.Run("SelectedStepHasNone", func(t *testing.T) { + t.Parallel() + + _, reused, err := intake.SelectedStepNodes(outputsDAG(), []string{"Log in", "fetch", "label", "report"}, source, nil, nil, nil) + require.NoError(t, err) + require.Empty(t, reused) + }) +} + +// A step can carry its outputs from a run of its own, in place of the main +// source: the seeding is the same, the record names that run with the times +// that the step had there, and the checks name the step and the run. +func TestSelectedStepNodesFromRuns(t *testing.T) { + t.Parallel() + + carried := `{"token":"old"}` + newer := `{"token":"newer"}` + main := &ir.DAGRunStatus{ + DAGRunID: "main", + Status: ir.Succeeded, + Nodes: []*ir.Node{ + {Step: ir.Step{Name: "Log in"}, Status: ir.NodeSucceeded, StepOutputsValue: &carried, StartedAt: "2026-10-26T09:00:00Z", FinishedAt: "2026-10-26T09:00:10Z"}, + {Step: ir.Step{Name: "fetch"}, Status: ir.NodeSucceeded, StartedAt: "2026-10-26T09:00:10Z", FinishedAt: "2026-10-26T09:00:40Z"}, + }, + } + other := &ir.DAGRunStatus{ + DAGRunID: "other", + Status: ir.Failed, + Nodes: []*ir.Node{ + {Step: ir.Step{Name: "Log in"}, Status: ir.NodeSucceeded, StepOutputsValue: &newer, StartedAt: "2026-10-26T11:00:00Z", FinishedAt: "2026-10-26T11:00:20Z"}, + {Step: ir.Step{Name: "fetch"}, Status: ir.NodeFailed}, + }, + } + + t.Run("SeedsTheStepFromItsRun", func(t *testing.T) { + t.Parallel() + + nodes, reused, err := intake.SelectedStepNodes(outputsDAG(), []string{"report"}, main, intake.RunStepSources{"login": other}, nil, nil) + require.NoError(t, err) + require.JSONEq(t, newer, *nodes[0].State.StepOutputsValue) + require.Equal(t, []ir.ReusedInput{ + {Step: "Log in", RunID: "other", CapturedAt: "2026-10-26T11:00:20Z", StartedAt: "2026-10-26T11:00:00Z", FinishedAt: "2026-10-26T11:00:20Z", Via: ir.ReusedInputViaRun}, + {Step: "fetch", RunID: "main", CapturedAt: "2026-10-26T09:00:40Z", StartedAt: "2026-10-26T09:00:10Z", FinishedAt: "2026-10-26T09:00:40Z", Via: ir.ReusedInputViaRun}, + }, reused) + }) + + t.Run("WorksWithoutMainSource", func(t *testing.T) { + t.Parallel() + + nodes, reused, err := intake.SelectedStepNodes(outputsDAG(), []string{"report"}, nil, intake.RunStepSources{"Log in": other}, nil, nil) + require.NoError(t, err) + require.JSONEq(t, newer, *nodes[0].State.StepOutputsValue) + require.Len(t, reused, 1) + require.Equal(t, "other", reused[0].RunID) + }) + + t.Run("SuppliedValuesStillWin", func(t *testing.T) { + t.Parallel() + + outputs := map[string]map[string]string{"login": {"token": "mine"}} + sources := map[string]intake.OutputSource{"login": {RunID: "gone"}} + _, reused, err := intake.SelectedStepNodes(outputsDAG(), []string{"report"}, main, intake.RunStepSources{"login": other}, outputs, sources) + require.NoError(t, err) + require.Equal(t, ir.ReusedInputViaCache, reused[0].Via) + require.Equal(t, "gone", reused[0].RunID) + }) + + tests := []struct { + name string + steps []string + from intake.RunStepSources + wantErr string + }{ + { + name: "StepDidNotSucceed", + steps: []string{"report"}, + from: intake.RunStepSources{"fetch": other}, + wantErr: "step fetch cannot be reused from run other: the step did not succeed there", + }, + { + name: "RunNotFinished", + steps: []string{"report"}, + from: intake.RunStepSources{"fetch": {DAGRunID: "busy", Status: ir.Running}}, + wantErr: "step fetch cannot be reused from run busy: the run is running, not finished", + }, + { + name: "StepNotInRun", + steps: []string{"report"}, + from: intake.RunStepSources{"fetch": {DAGRunID: "thin", Status: ir.Succeeded}}, + wantErr: "step fetch cannot be reused from run thin: the run has no such step", + }, + { + name: "StepSelectedToRun", + steps: []string{"fetch"}, + from: intake.RunStepSources{"fetch": main}, + wantErr: `outputsFromRuns names step "fetch", which is selected to run`, + }, + { + name: "UnknownStep", + steps: []string{"report"}, + from: intake.RunStepSources{"missing": main}, + wantErr: `unknown step "missing"`, + }, + { + name: "NameAndIDOfOneStep", + steps: []string{"report"}, + from: intake.RunStepSources{"login": main, "Log in": main}, + wantErr: "is given by both", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + _, _, err := intake.SelectedStepNodes(outputsDAG(), tt.steps, nil, tt.from, nil, nil) + require.ErrorContains(t, err, tt.wantErr) + }) + } +} + +// A selected-steps run calls the end-of-run handlers only when the selection +// has a graph end step: a step that no other step depends on. +func TestSelectionSkipsHandlers(t *testing.T) { + t.Parallel() + + chain := &ir.DAG{Steps: []ir.Step{ + {Name: "a", ID: "a_id"}, + {Name: "b", Depends: []string{"a"}}, + {Name: "c", Depends: []string{"b"}}, + }} + diamond := &ir.DAG{Steps: []ir.Step{ + {Name: "a"}, + {Name: "b", Depends: []string{"a"}}, + {Name: "c", Depends: []string{"a"}}, + {Name: "d", Depends: []string{"b"}}, + }} + inferred := &ir.DAG{Steps: []ir.Step{ + {Name: "produce"}, + {Name: "use", InferredDepends: []ir.InferredDependency{{Step: "produce"}}}, + }} + + tests := []struct { + name string + dag *ir.DAG + steps []string + want bool + wantErr string + }{ + {name: "InteriorStep", dag: chain, steps: []string{"b"}, want: true}, + {name: "EndStep", dag: chain, steps: []string{"c"}, want: false}, + {name: "FirstAndInterior", dag: chain, steps: []string{"a", "b"}, want: true}, + {name: "InteriorAndEnd", dag: chain, steps: []string{"a", "c"}, want: false}, + {name: "ByID", dag: chain, steps: []string{"a_id"}, want: true}, + {name: "DiamondEndStepOne", dag: diamond, steps: []string{"c"}, want: false}, + {name: "DiamondEndStepTwo", dag: diamond, steps: []string{"d"}, want: false}, + {name: "DiamondInterior", dag: diamond, steps: []string{"a", "b"}, want: true}, + {name: "InferredProducerIsInterior", dag: inferred, steps: []string{"produce"}, want: true}, + {name: "InferredConsumerIsEnd", dag: inferred, steps: []string{"use"}, want: false}, + {name: "Unknown", dag: chain, steps: []string{"x"}, wantErr: `unknown step "x"`}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := intake.SelectionSkipsHandlers(tt.dag, tt.steps) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/persis/dag.go b/internal/persis/dag.go index 2a754def93..75ef22bc58 100644 --- a/internal/persis/dag.go +++ b/internal/persis/dag.go @@ -47,6 +47,11 @@ type DAGDefinitionStore interface { Delete(ctx context.Context, id string) error Get(ctx context.Context, id string) (DAGDefinition, error) Update(ctx context.Context, id string, source []byte) error + // UpdateIfMatch is Update for a source that is still the one with the + // SHA-256 expectedSHA256, in hex. Otherwise it writes nothing and returns an + // error that wraps ErrConflict. The check and the write are atomic for every + // process that shares the store. + UpdateIfMatch(ctx context.Context, id, expectedSHA256 string, source []byte) error Rename(ctx context.Context, oldID, newID string) error GetMetadata(ctx context.Context, id string) (*ir.DAG, error) Catalog(ctx context.Context) (DAGCatalog, error) diff --git a/internal/persis/dag_repository.go b/internal/persis/dag_repository.go index 81861566ba..a1940eacc5 100644 --- a/internal/persis/dag_repository.go +++ b/internal/persis/dag_repository.go @@ -93,6 +93,24 @@ func (r *DAGRepository) LoadSpec(ctx context.Context, source []byte, name string } func (r *DAGRepository) UpdateSpec(ctx context.Context, id string, source []byte) error { + if err := r.validateSpec(ctx, id, source); err != nil { + return err + } + return r.store.Update(ctx, id, source) +} + +// UpdateSpecIfMatch is UpdateSpec for a source that was read with the SHA-256 +// expectedSHA256. When the source changed since, it writes nothing and +// returns an error that wraps ErrConflict. +func (r *DAGRepository) UpdateSpecIfMatch(ctx context.Context, id, expectedSHA256 string, source []byte) error { + if err := r.validateSpec(ctx, id, source); err != nil { + return err + } + return r.store.UpdateIfMatch(ctx, id, expectedSHA256, source) +} + +// validateSpec loads source as the DAG id and validates it. +func (r *DAGRepository) validateSpec(ctx context.Context, id string, source []byte) error { definition, err := r.store.Get(ctx, id) if err != nil { return fmt.Errorf("failed to locate DAG %s: %w", id, err) @@ -110,10 +128,7 @@ func (r *DAGRepository) UpdateSpec(ctx context.Context, id string, source []byte if err != nil { return err } - if err := dag.Validate(); err != nil { - return err - } - return r.store.Update(ctx, id, source) + return dag.Validate() } func (r *DAGRepository) SetSuspended(ctx context.Context, id string, suspended bool) error { diff --git a/internal/persis/dag_repository_test.go b/internal/persis/dag_repository_test.go index fd7b6982fc..e9b9f194b9 100644 --- a/internal/persis/dag_repository_test.go +++ b/internal/persis/dag_repository_test.go @@ -43,6 +43,10 @@ func (s dagDefinitionStoreStub) Update(ctx context.Context, id string, source [] return s.update(ctx, id, source) } +func (s dagDefinitionStoreStub) UpdateIfMatch(ctx context.Context, id, _ string, source []byte) error { + return s.Update(ctx, id, source) +} + func TestDAGRepositoryLoadsDefinitionWithoutSourcePath(t *testing.T) { repository := NewDAGRepository(dagDefinitionStoreStub{ definition: DAGDefinition{ diff --git a/internal/persis/file/dag/store.go b/internal/persis/file/dag/store.go index c84ce87a48..716f943c38 100644 --- a/internal/persis/file/dag/store.go +++ b/internal/persis/file/dag/store.go @@ -15,7 +15,9 @@ import ( "sort" "strings" "sync" + "time" + "github.com/dagucloud/dagu/v2/internal/cmn/dirlock" "github.com/dagucloud/dagu/v2/internal/cmn/fileutil" "github.com/dagucloud/dagu/v2/internal/cmn/logger" "github.com/dagucloud/dagu/v2/internal/cmn/logger/tag" @@ -29,6 +31,13 @@ import ( var _ persis.DAGDefinitionStore = (*Store)(nil) +// The lock that orders the writes of a DAG file. +const ( + specLockDirName = "spec-locks" + specLockStaleThreshold = 30 * time.Second + specLockRetryInterval = 50 * time.Millisecond +) + // Option configures filesystem DAG definition storage. type Option func(*Options) @@ -373,6 +382,64 @@ func (store *Store) Update(ctx context.Context, name string, yamlSpec []byte) er if resolved.ExternalSymlink { return fmt.Errorf("%w: external DAG file symlink", persis.ErrDAGReadOnly) } + switch unlock, err := store.lockSpec(ctx, name, resolved); { + case err == nil: + defer unlock() + case ctx.Err() != nil: + return err + default: + // A lock directory that cannot hold the lock fails every conditional + // write as well, so no write can race with this one. + logger.Warn(ctx, "Writing DAG file without the spec lock", tag.Name(name), tag.Error(err)) + } + return store.write(ctx, resolved, yamlSpec) +} + +// UpdateIfMatch replaces the DAG file when it still has the SHA-256 +// expectedSHA256. The check and the write hold the lock of the file that +// Update takes too, so no other write lands between them. +func (store *Store) UpdateIfMatch(ctx context.Context, name, expectedSHA256 string, yamlSpec []byte) error { + resolved, err := store.locateDAG(ctx, name) + if err != nil { + return fmt.Errorf("failed to locate DAG %s: %w", name, err) + } + if resolved.ExternalSymlink { + return fmt.Errorf("%w: external DAG file symlink", persis.ErrDAGReadOnly) + } + unlock, err := store.lockSpec(ctx, name, resolved) + if err != nil { + return err + } + defer unlock() + + current, err := fileutil.ReadFile(resolved.ResolvedPath) + if err != nil { + return err + } + sum := sha256.Sum256(current) + if actual := hex.EncodeToString(sum[:]); actual != expectedSHA256 { + return fmt.Errorf("%w: DAG %s has sha256 %s", persis.ErrConflict, name, actual) + } + return store.write(ctx, resolved, yamlSpec) +} + +// lockSpec takes the lock that orders the writes of a located DAG file. The +// lock lives in the directory of the index, which every process that shares +// the store uses. The returned function releases it. +func (store *Store) lockSpec(ctx context.Context, name string, resolved ResolvedFile) (func(), error) { + key := sha256.Sum256([]byte(resolved.ResolvedPath)) + lock := dirlock.New(filepath.Join(filepath.Dir(store.indexPath), specLockDirName, hex.EncodeToString(key[:8])), &dirlock.LockOptions{ + StaleThreshold: specLockStaleThreshold, + RetryInterval: specLockRetryInterval, + }) + if err := lock.Lock(ctx); err != nil { + return nil, fmt.Errorf("failed to lock DAG %s: %w", name, err) + } + return func() { _ = lock.Unlock() }, nil +} + +// write replaces a located DAG file and announces the change. +func (store *Store) write(ctx context.Context, resolved ResolvedFile, yamlSpec []byte) error { if err := fileutil.WriteFileAtomic(resolved.ResolvedPath, yamlSpec, defaultPerm); err != nil { return err } diff --git a/internal/persis/file/dag/store_test.go b/internal/persis/file/dag/store_test.go index d17e5b54d2..20e38d9fad 100644 --- a/internal/persis/file/dag/store_test.go +++ b/internal/persis/file/dag/store_test.go @@ -5,6 +5,8 @@ package dag import ( "context" + "crypto/sha256" + "encoding/hex" "fmt" "os" "path/filepath" @@ -1069,6 +1071,78 @@ steps: require.Error(t, err) } +// Two saves from the same read of a DAG: one writes, the other gets a +// conflict and writes nothing. +func TestUpdateSpecIfMatch(t *testing.T) { + tmpDir := fileutil.MustTempDir("test-update-spec-if-match") + defer func() { + _ = os.RemoveAll(tmpDir) + }() + + store := newRepository(tmpDir, WithSkipExamples(true)) + ctx := context.Background() + initial := "name: cas-dag\nsteps:\n - name: step1\n run: echo initial\n" + require.NoError(t, store.Create(ctx, "cas-dag", []byte(initial))) + sum := sha256.Sum256([]byte(initial)) + base := hex.EncodeToString(sum[:]) + + contents := []string{ + "name: cas-dag\nsteps:\n - name: step1\n run: echo first\n", + "name: cas-dag\nsteps:\n - name: step1\n run: echo second\n", + } + errs := make([]error, len(contents)) + var wg sync.WaitGroup + for i, content := range contents { + wg.Go(func() { errs[i] = store.UpdateSpecIfMatch(ctx, "cas-dag", base, []byte(content)) }) + } + wg.Wait() + + var won []string + for i, err := range errs { + if err == nil { + won = append(won, contents[i]) + continue + } + require.ErrorIs(t, err, persis.ErrConflict) + } + require.Len(t, won, 1) + spec, err := store.GetSpec(ctx, "cas-dag") + require.NoError(t, err) + assert.Equal(t, won[0], spec) +} + +// A plain update waits while a conditional update holds the lock of the file, +// so it cannot land between the hash check and the write. +func TestUpdateWaitsForSpecLock(t *testing.T) { + store := NewStore(t.TempDir(), WithSkipExamples(true)) + ctx := context.Background() + initial := "name: lock-dag\nsteps:\n - name: step1\n run: echo initial\n" + require.NoError(t, store.Create(ctx, "lock-dag", []byte(initial))) + resolved, err := store.locateDAG(ctx, "lock-dag") + require.NoError(t, err) + + unlock, err := store.lockSpec(ctx, "lock-dag", resolved) + require.NoError(t, err) + next := "name: lock-dag\nsteps:\n - name: step1\n run: echo next\n" + done := make(chan error, 1) + go func() { done <- store.Update(ctx, "lock-dag", []byte(next)) }() + select { + case err := <-done: + unlock() + t.Fatalf("Update returned while the lock was held: %v", err) + case <-time.After(300 * time.Millisecond): + } + current, err := os.ReadFile(resolved.ResolvedPath) + require.NoError(t, err) + assert.Equal(t, initial, string(current)) + + unlock() + require.NoError(t, <-done) + current, err = os.ReadFile(resolved.ResolvedPath) + require.NoError(t, err) + assert.Equal(t, next, string(current)) +} + func TestDelete(t *testing.T) { tmpDir := fileutil.MustTempDir("test-delete") defer func() { diff --git a/internal/persis/file/dagrun/dagrun.go b/internal/persis/file/dagrun/dagrun.go index effd2a8e10..3158e2d26d 100644 --- a/internal/persis/file/dagrun/dagrun.go +++ b/internal/persis/file/dagrun/dagrun.go @@ -12,6 +12,7 @@ import ( "os" "path/filepath" "regexp" + "slices" "sort" "strings" "time" @@ -482,7 +483,7 @@ func (dr DAGRun) listLogFiles(ctx context.Context) ([]string, error) { return nil, fmt.Errorf("failed to list attempt directories: %w", err) } - var logFiles []string + var logFiles, nodeLogs, logDirs []string for _, attDir := range attDirs { attempt, err := NewAttempt(filepath.Join(dr.baseDir, attDir, JSONLStatusFile), nil) if err != nil { @@ -504,17 +505,34 @@ func (dr DAGRun) listLogFiles(ctx context.Context) ([]string, error) { continue } logFiles = append(logFiles, status.Log) + if status.Log != "" { + logDirs = append(logDirs, filepath.Dir(status.Log)) + } for _, n := range status.NodesInRunOrder() { if n == nil { continue } - logFiles = append(logFiles, n.Stdout, n.Stderr) + nodeLogs = append(nodeLogs, n.Stdout, n.Stderr) + } + } + // A step skipped in a run seeded from another run names the logs of that + // run, which stay with it. The logs of the run's own attempts lie in the + // directories of their scheduler logs. + for _, file := range nodeLogs { + if slices.ContainsFunc(logDirs, func(dir string) bool { return pathWithin(file, dir) }) { + logFiles = append(logFiles, file) } } return logFiles, nil } +// pathWithin reports whether path lies inside dir. +func pathWithin(path, dir string) bool { + rel, err := filepath.Rel(dir, path) + return err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) +} + func (dr DAGRun) listArtifactDirs(ctx context.Context) ([]string, error) { attDirs, err := dr.listAttemptDirs() if err != nil { diff --git a/internal/persis/file/dagrun/dagrun_test.go b/internal/persis/file/dagrun/dagrun_test.go index d83cf70b57..c1f485a826 100644 --- a/internal/persis/file/dagrun/dagrun_test.go +++ b/internal/persis/file/dagrun/dagrun_test.go @@ -417,6 +417,58 @@ func TestRemoveLogFiles(t *testing.T) { } }) + // A run seeded from another run, such as a selected-steps run, names the + // logs of the source run for its skipped steps. Those logs stay with the + // source run. The logs of an earlier attempt of the same run go. + t.Run("KeepsLogsOfAnotherRun", func(t *testing.T) { + tmpDir := t.TempDir() + path := func(dir, name string) string { return filepath.Join(tmpDir, dir, name) } + source := []string{path("source", "producer.out"), path("source", "producer.err")} + own := []string{ + path("first", "scheduler.log"), path("first", "consumer.out"), + path("second", "scheduler.log"), + } + for _, logFile := range append(append([]string{}, source...), own...) { + require.NoError(t, os.MkdirAll(filepath.Dir(logFile), 0750)) + require.NoError(t, os.WriteFile(logFile, []byte("test log content"), 0600)) + } + + root := setupTestDataRoot(t) + run := root.CreateTestDAGRun(t, "test-dag-run", persis.NewUTC(time.Now())) + dag := &ir.DAG{Name: "test-dag"} + producer := &ir.Node{ + Step: ir.Step{Name: "producer"}, Status: ir.NodeSkipped, SkippedByRetry: true, + Stdout: source[0], Stderr: source[1], + } + write := func(ts time.Time, log string, consumer *ir.Node) { + status := ir.InitialStatus(dag) + status.DAGRunID = "test-dag-run" + status.Status = ir.Succeeded + status.Log = log + status.Nodes = []*ir.Node{producer, consumer} + att, err := run.CreateAttempt(run.Context, persis.NewUTC(ts), nil, "") + require.NoError(t, err) + require.NoError(t, att.Open(run.Context)) + require.NoError(t, att.Write(run.Context, status)) + require.NoError(t, att.Close(run.Context)) + } + now := time.Now() + write(now, own[0], &ir.Node{Step: ir.Step{Name: "consumer"}, Stdout: own[1]}) + // The retry carries the consumer of the first attempt. + write(now.Add(time.Second), own[2], &ir.Node{ + Step: ir.Step{Name: "consumer"}, Status: ir.NodeSkipped, SkippedByRetry: true, Stdout: own[1], + }) + + require.NoError(t, run.removeLogFiles(run.Context)) + + for _, logFile := range source { + assert.FileExists(t, logFile) + } + for _, logFile := range own { + assert.NoFileExists(t, logFile) + } + }) + t.Run("Idempotent", func(t *testing.T) { dagRunLogDir := filepath.Join(t.TempDir(), "dag-run") attemptLogDir := filepath.Join(dagRunLogDir, "attempt") diff --git a/internal/samples/reader.go b/internal/samples/reader.go index 0784954840..388b9003e8 100644 --- a/internal/samples/reader.go +++ b/internal/samples/reader.go @@ -6,6 +6,7 @@ package samples import ( "context" "errors" + "fmt" "io/fs" "os" "slices" @@ -225,6 +226,9 @@ func (r *reader) Events(ctx context.Context, q EventQuery) (EventPage, error) { f, err := os.Open(r.st.eventsPath()) if err != nil { if errors.Is(err, fs.ErrNotExist) { + if before >= 0 { + return page, fmt.Errorf("%w %q", ErrInvalidCursor, q.Before) + } return page, nil } return page, err @@ -236,7 +240,14 @@ func (r *reader) Events(ctx context.Context, q EventQuery) (EventPage, error) { } end := st.Size() if before >= 0 { - end = min(end, before) + issued, err := isEventStart(f, before, end) + if err != nil { + return page, err + } + if !issued { + return page, fmt.Errorf("%w %q", ErrInvalidCursor, q.Before) + } + end = before } limit := q.Limit if limit <= 0 { @@ -272,6 +283,20 @@ func (r *reader) Events(ctx context.Context, q EventQuery) (EventPage, error) { return page, ctx.Err() } +// isEventStart reports whether off can be a cursor of the events file of the +// given size: the start of a line that follows another line. The file only +// grows, so every cursor that the store gave stays one. +func isEventStart(f *os.File, off, size int64) (bool, error) { + if off <= 0 || off >= size { + return false, nil + } + b := make([]byte, 1) + if _, err := f.ReadAt(b, off-1); err != nil { + return false, err + } + return b[0] == '\n', nil +} + type count struct{ n, f uint64 } func (c *count) add(o count) { c.n += o.n; c.f += o.f } diff --git a/internal/samples/store_test.go b/internal/samples/store_test.go index 89bedb4d94..06989a8eb8 100644 --- a/internal/samples/store_test.go +++ b/internal/samples/store_test.go @@ -250,9 +250,16 @@ func TestEvents(t *testing.T) { require.Len(t, w.Events, 3) _, err = rd.Events(ctx, EventQuery{Before: "bad"}) require.ErrorIs(t, err, ErrInvalidCursor) + // A cursor that decodes but that the store did not give is rejected too. + for _, c := range []string{encodeCursor(0), encodeCursor(1), encodeCursor(1 << 40)} { + _, err = rd.Events(ctx, EventQuery{Before: c}) + require.ErrorIs(t, err, ErrInvalidCursor, c) + } // A store without events still rejects a cursor it did not make. - _, err = OpenReader(filepath.Join(t.TempDir(), "none")).Events(ctx, EventQuery{Before: "bad"}) - require.ErrorIs(t, err, ErrInvalidCursor) + for _, c := range []string{"bad", encodeCursor(5)} { + _, err = OpenReader(filepath.Join(t.TempDir(), "none")).Events(ctx, EventQuery{Before: c}) + require.ErrorIs(t, err, ErrInvalidCursor, c) + } } // An outage goes on over a restart: no second failed event, one recovered event with the whole outage. diff --git a/internal/service/frontend/api/v1/api.go b/internal/service/frontend/api/v1/api.go index 4dc11e4b36..d51abae0b9 100644 --- a/internal/service/frontend/api/v1/api.go +++ b/internal/service/frontend/api/v1/api.go @@ -42,6 +42,7 @@ import ( "github.com/dagucloud/dagu/v2/internal/queue" "github.com/dagucloud/dagu/v2/internal/remotenode" "github.com/dagucloud/dagu/v2/internal/runtime" + "github.com/dagucloud/dagu/v2/internal/samples" "github.com/dagucloud/dagu/v2/internal/schedulerstate" secretpkg "github.com/dagucloud/dagu/v2/internal/secret" authservice "github.com/dagucloud/dagu/v2/internal/service/auth" @@ -97,6 +98,8 @@ type API struct { defaultExecMode config.ExecutionMode dagWritesDisabled bool // True when git sync read-only mode is active baseConfigStore dagsettings.BaseConfigStore + // samplesReader opens a samples store; nil means samples.OpenReader. + samplesReader func(dir string) samples.Reader dagSettingsStore dagsettings.Store dagPinStore dagpin.Store wikiStore wiki.PageStore diff --git a/internal/service/frontend/api/v1/dagruns.go b/internal/service/frontend/api/v1/dagruns.go index 7f35aa752e..f5d9155c03 100644 --- a/internal/service/frontend/api/v1/dagruns.go +++ b/internal/service/frontend/api/v1/dagruns.go @@ -192,7 +192,7 @@ func (a *API) ExecuteDAGRunFromSpec(ctx context.Context, request api.ExecuteDAGR } } - selection, err := selectedStepsFromBody(request.Body.Steps, request.Body.OutputsFromRunId, request.Body.Outputs) + selection, err := selectedStepsFromBody(request.Body.Steps, request.Body.OutputsFromRunId, request.Body.OutputsFromRuns, request.Body.Outputs, request.Body.OutputsSources, request.Body.Ephemeral) if err != nil { return nil, err } @@ -1060,35 +1060,39 @@ func (a *API) DownloadDAGRunArtifact(ctx context.Context, request api.DownloadDA }, nil } -func (a *API) GetDAGRunOutputs(ctx context.Context, request api.GetDAGRunOutputsRequestObject) (api.GetDAGRunOutputsResponseObject, error) { - var attempt dagrun.Attempt - var err error - - if request.DagRunId == "latest" { - attempt, err = a.dagRunRepository.LatestAttempt(ctx, request.Name, persis.DAGRunLatestAttemptOptions{}) +// findVisibleAttempt resolves a DAG-run attempt by run ID ("latest" allowed) +// and checks that its workspace is visible. A non-empty notFound message means +// that the run does not exist. +func (a *API) findVisibleAttempt(ctx context.Context, name, dagRunID string) (attempt dagrun.Attempt, notFound string, err error) { + if dagRunID == "latest" { + attempt, err = a.dagRunRepository.LatestAttempt(ctx, name, persis.DAGRunLatestAttemptOptions{}) if err != nil { - return api.GetDAGRunOutputs404JSONResponse{ - Code: api.ErrorCodeNotFound, - Message: fmt.Sprintf("no dag-runs found for DAG %s", request.Name), - }, nil + return nil, fmt.Sprintf("no dag-runs found for DAG %s", name), nil } } else { - ref := ir.NewDAGRunRef(request.Name, request.DagRunId) - attempt, err = a.dagRunRepository.FindAttempt(ctx, ref) + attempt, err = a.dagRunRepository.FindAttempt(ctx, ir.NewDAGRunRef(name, dagRunID)) if err != nil { - return api.GetDAGRunOutputs404JSONResponse{ - Code: api.ErrorCodeNotFound, - Message: fmt.Sprintf("dag-run ID %s not found for DAG %s", request.DagRunId, request.Name), - }, nil + return nil, fmt.Sprintf("dag-run ID %s not found for DAG %s", dagRunID, name), nil } } workspaceName, err := workspaceNameForAttempt(ctx, attempt) if err != nil { - return nil, err + return nil, "", err } if err := a.requireWorkspaceVisible(ctx, workspaceName); err != nil { + return nil, "", err + } + return attempt, "", nil +} + +func (a *API) GetDAGRunOutputs(ctx context.Context, request api.GetDAGRunOutputsRequestObject) (api.GetDAGRunOutputsResponseObject, error) { + attempt, notFound, err := a.findVisibleAttempt(ctx, request.Name, request.DagRunId) + if err != nil { return nil, err } + if notFound != "" { + return api.GetDAGRunOutputs404JSONResponse{Code: api.ErrorCodeNotFound, Message: notFound}, nil + } outputs, err := attempt.ReadOutputs(ctx) if err != nil { @@ -3100,6 +3104,9 @@ func (a *API) retryDAGRun(ctx context.Context, dagName, dagRunID, retryDagRunID, if prevStatus == nil { return retryDAGRunResult{}, fmt.Errorf("error reading status: status data is nil") } + if err := rejectEphemeralRun(prevStatus, "retried"); err != nil { + return retryDAGRunResult{}, err + } if prevStatus.Status.IsActive() { message := fmt.Sprintf("DAG-run %s is active and cannot be retried", prevStatus.DAGRun()) if prevStatus.Status == ir.Waiting { @@ -3565,6 +3572,9 @@ func (a *API) rescheduleDAGRun(ctx context.Context, dagName, dagRunID string, op if status == nil { return rescheduleDAGRunResult{}, fmt.Errorf("failed to read status: status data is nil") } + if err := rejectEphemeralRun(status, "rescheduled"); err != nil { + return rescheduleDAGRunResult{}, err + } if err := a.requireDAGRunStatusExecute(ctx, status); err != nil { return rescheduleDAGRunResult{}, err } @@ -4045,7 +4055,7 @@ func applyRejection(ctx context.Context, node *ir.Node, status *ir.DAGRunStatus, } status.Status = ir.Rejected - status.FinishedAt = time.Now().Format(time.RFC3339) + status.FinishedAt = stringutil.FormatTime(time.Now()) } // resumeWaitingDAGRun accepts a manual resume only while its attempt and status diff --git a/internal/service/frontend/api/v1/dagruns_edit_retry.go b/internal/service/frontend/api/v1/dagruns_edit_retry.go index baa5b0049b..9b5fe149b0 100644 --- a/internal/service/frontend/api/v1/dagruns_edit_retry.go +++ b/internal/service/frontend/api/v1/dagruns_edit_retry.go @@ -275,6 +275,9 @@ func (a *API) buildEditRetryPlan( if status == nil { return nil, nil, fmt.Errorf("failed to read status: status data is nil") } + if err := rejectEphemeralRun(status, "edited"); err != nil { + return nil, nil, err + } if status.Status.IsActive() || status.Status == ir.NotStarted { return nil, nil, badEditRetryRequest(fmt.Sprintf("dag-run %s is %s and cannot be edit-retried", sourceDAGRunID, status.Status.String())) } diff --git a/internal/service/frontend/api/v1/dags.go b/internal/service/frontend/api/v1/dags.go index 998887f374..0d72fcc1aa 100644 --- a/internal/service/frontend/api/v1/dags.go +++ b/internal/service/frontend/api/v1/dags.go @@ -5,6 +5,8 @@ package api import ( "context" + "crypto/sha256" + "encoding/hex" "errors" "fmt" "log/slog" @@ -101,9 +103,9 @@ func (a *API) ValidateDAGSpec(ctx context.Context, request api.ValidateDAGSpecRe } // ValidateDAGSpecData validates a DAG spec and returns its canonical form. -func (a *API) ValidateDAGSpecData(ctx context.Context, name, spec string) (*api.ValidateDAGSpec200JSONResponse, *ir.DAG, error) { +func (a *API) ValidateDAGSpecData(ctx context.Context, name, source string) (*api.ValidateDAGSpec200JSONResponse, *ir.DAG, error) { dag, err := a.dagRepository.LoadSpec(ctx, - []byte(spec), + []byte(source), name, persis.DAGLoadOptions{AllowBuildErrors: true}, ) @@ -123,10 +125,15 @@ func (a *API) ValidateDAGSpecData(ctx context.Context, name, spec string) (*api. details := toDAGDetails(dag) + notices := a.specValueReferenceNotices(ctx, name, source) + response := &api.ValidateDAGSpec200JSONResponse{ - Valid: len(errs) == 0, - Dag: details, - Errors: errs, + Valid: len(errs) == 0, + Dag: details, + Errors: errs, + Issues: buildValidationIssues(err, dag, notices), + ValueReferenceNotices: toAPIValueReferenceNotices(notices), + ReferenceScope: buildReferenceScope(dag), } if dag != nil { response.Warnings = dag.BuildWarnings @@ -300,6 +307,7 @@ func (a *API) GetDAGSpec(ctx context.Context, request api.GetDAGSpecRequestObjec return &api.GetDAGSpec200JSONResponse{ Dag: details, Spec: yamlSpec, + Sha256: specSHA256(yamlSpec), Errors: errs, Warnings: dag.BuildWarnings, ValueReferenceNotices: valueReferenceNotices, @@ -340,6 +348,16 @@ func (a *API) UpdateDAGSpec(ctx context.Context, request api.UpdateDAGSpecReques if err := a.requireDAGWriteForWorkspace(ctx, dagWorkspaceName(currentDAG)); err != nil { return nil, err } + expected := request.Body.ExpectedSha256 + if expected != nil { + current, err := a.dagRepository.GetSpec(ctx, request.FileName) + if err != nil { + return nil, err + } + if actual := specSHA256(current); actual != *expected { + return specConflict(request.FileName, *expected, actual), nil + } + } nextDAG, err := a.dagRepository.LoadSpec(ctx, []byte(request.Body.Spec), string(request.FileName), @@ -354,7 +372,20 @@ func (a *API) UpdateDAGSpec(ctx context.Context, request api.UpdateDAGSpecReques } } - err = a.dagRepository.UpdateSpec(ctx, request.FileName, []byte(request.Body.Spec)) + if expected != nil { + // The check above answers early; this one holds against a write that + // lands after it. + err = a.dagRepository.UpdateSpecIfMatch(ctx, request.FileName, *expected, []byte(request.Body.Spec)) + } else { + err = a.dagRepository.UpdateSpec(ctx, request.FileName, []byte(request.Body.Spec)) + } + if errors.Is(err, persis.ErrConflict) { + current, readErr := a.dagRepository.GetSpec(ctx, request.FileName) + if readErr != nil { + return nil, readErr + } + return specConflict(request.FileName, *expected, specSHA256(current)), nil + } var errs []string @@ -369,11 +400,35 @@ func (a *API) UpdateDAGSpec(ctx context.Context, request api.UpdateDAGSpecReques a.logAudit(ctx, audit.CategoryDAG, "dag_update", map[string]any{"dag_name": request.FileName}) a.notifyDAGMutation(request.FileName) + written := request.Body.Spec + if len(errs) > 0 { + // The repository rejected the spec, so the file still holds the old bytes. + current, err := a.dagRepository.GetSpec(ctx, request.FileName) + if err != nil { + return nil, err + } + written = current + } return api.UpdateDAGSpec200JSONResponse{ Errors: errs, + Sha256: specSHA256(written), }, nil } +// specConflict answers a save of a spec that changed since the client read it. +func specConflict(fileName, expected, actual string) api.UpdateDAGSpec409JSONResponse { + return api.UpdateDAGSpec409JSONResponse{ + Code: api.ErrorCodeConflict, + Message: fmt.Sprintf("DAG %s changed since it was read: expected sha256 %s, current sha256 %s", fileName, expected, actual), + } +} + +// specSHA256 returns the lowercase hex SHA-256 of the spec bytes. +func specSHA256(spec string) string { + sum := sha256.Sum256([]byte(spec)) + return hex.EncodeToString(sum[:]) +} + func (a *API) RenameDAG(ctx context.Context, request api.RenameDAGRequestObject) (api.RenameDAGResponseObject, error) { if a.dagWritesDisabled { return nil, errDAGWritesDisabled @@ -449,7 +504,7 @@ func (a *API) GetDAGDAGRunHistory(ctx context.Context, request api.GetDAGDAGRunH return nil, err } dagName := a.resolveDAGName(ctx, request.FileName) - response, err := a.buildDAGRunHistoryResponse(ctx, dag, dagName) + response, err := a.buildDAGRunHistoryResponse(ctx, dag, dagName, valueOf(request.Params.IncludeEphemeral)) if err != nil { return nil, &Error{ HTTPStatus: http.StatusInternalServerError, @@ -464,8 +519,9 @@ func (a *API) buildDAGRunHistoryResponse( ctx context.Context, dag *ir.DAG, dagName string, + includeEphemeral bool, ) (api.GetDAGDAGRunHistory200JSONResponse, error) { - recentHistory, err := a.dagRunRepository.RecentStatuses(ctx, dagName, defaultHistoryLimit) + recentHistory, err := a.dagRunRepository.RecentStatusesWithOptions(ctx, dagName, defaultHistoryLimit, persis.RecentStatusesOptions{IncludeEphemeral: includeEphemeral}) if err != nil { return api.GetDAGDAGRunHistory200JSONResponse{}, fmt.Errorf("list recent DAG runs for %s: %w", dagName, err) } @@ -1041,7 +1097,7 @@ func (a *API) ExecuteDAG(ctx context.Context, request api.ExecuteDAGRequestObjec } } - selection, err := selectedStepsFromBody(request.Body.Steps, request.Body.OutputsFromRunId, request.Body.Outputs) + selection, err := selectedStepsFromBody(request.Body.Steps, request.Body.OutputsFromRunId, request.Body.OutputsFromRuns, request.Body.Outputs, request.Body.OutputsSources, request.Body.Ephemeral) if err != nil { return nil, err } @@ -2151,7 +2207,7 @@ func (a *API) GetDAGHistoryData(ctx context.Context, fileName string) (any, erro } dagName := a.resolveDAGName(readCtx, fileName) - return a.buildDAGRunHistoryResponse(readCtx, dag, dagName) + return a.buildDAGRunHistoryResponse(readCtx, dag, dagName, false) }) } diff --git a/internal/service/frontend/api/v1/dags_test.go b/internal/service/frontend/api/v1/dags_test.go index 2286cd0432..37c949da99 100644 --- a/internal/service/frontend/api/v1/dags_test.go +++ b/internal/service/frontend/api/v1/dags_test.go @@ -6,6 +6,8 @@ package api_test import ( "bytes" "context" + "crypto/sha256" + "encoding/hex" "fmt" "net/http" "os" @@ -2292,3 +2294,157 @@ steps: require.NotNil(t, notice.Class) require.Equal(t, api.ValueReferenceNoticeClassRuntimeOnly, *notice.Class) } + +const validateScopeSpec = ` +name: scope-spec +params: + - region: eu +consts: + - image: ${consts.missing} +steps: + - name: fetch + id: fetch + run: echo hi + outputs: + - name: body + type: string + - name: use + run: echo ${steps.fetch.outputs.body} + depends: [fetch] +` + +func TestValidateIssuesAndScope(t *testing.T) { + server := test.SetupServer(t) + name := "validate_scope" + + var body api.ValidateDAGSpec200JSONResponse + server.Client().Post("/api/v1/dags/validate", api.ValidateDAGSpecJSONRequestBody{ + Name: &name, Spec: validateScopeSpec, + }).ExpectStatus(http.StatusOK).Send(t).Unmarshal(t, &body) + + require.True(t, body.Valid) + require.Empty(t, body.Errors) + + require.Len(t, body.ValueReferenceNotices, 1) + require.NotNil(t, body.ValueReferenceNotices[0].FieldPath) + assert.Equal(t, "consts.image", *body.ValueReferenceNotices[0].FieldPath) + require.Len(t, body.Issues, 1) + assert.Equal(t, api.ValidationIssueSeverityNotice, body.Issues[0].Severity) + assert.Equal(t, api.ValidationIssueSourceValueReference, body.Issues[0].Source) + + require.NotNil(t, body.ReferenceScope) + scope := body.ReferenceScope + require.Len(t, scope.Params, 1) + assert.Equal(t, "${params.region}", scope.Params[0].Reference) + require.NotNil(t, scope.Params[0].DefaultValue) + assert.Equal(t, "eu", *scope.Params[0].DefaultValue) + require.Len(t, scope.Steps, 2) + fetch, use := scope.Steps[0], scope.Steps[1] + assert.Equal(t, "steps[0]", fetch.StepPath) + require.NotNil(t, fetch.Id) + assert.Equal(t, "fetch", *fetch.Id) + require.Len(t, fetch.Outputs, 1) + assert.Equal(t, "${steps.fetch.outputs.body}", fetch.Outputs[0].Reference) + assert.Equal(t, api.ReferenceScopeOutputSourceDeclared, fetch.Outputs[0].Source) + assert.Equal(t, api.ReferenceScopeStepOutputsKnownStatic, fetch.OutputsKnown) + assert.Equal(t, []string{"fetch"}, use.Upstream) +} + +func TestValidateIssuesInvalidSpec(t *testing.T) { + server := test.SetupServer(t) + name := "validate_invalid" + + var body api.ValidateDAGSpec200JSONResponse + server.Client().Post("/api/v1/dags/validate", api.ValidateDAGSpecJSONRequestBody{ + Name: &name, + Spec: "steps:\n - name: a\n run: echo a\n depends: [missing]\n", + }).ExpectStatus(http.StatusOK).Send(t).Unmarshal(t, &body) + + require.False(t, body.Valid) + require.NotEmpty(t, body.Errors) + var messages []string + for _, issue := range body.Issues { + if issue.Severity == api.ValidationIssueSeverityError { + messages = append(messages, issue.Message) + } + } + assert.Equal(t, body.Errors, messages) +} + +func TestValidateIssuesBaseConfig(t *testing.T) { + server := test.SetupServer(t) + require.NoError(t, os.WriteFile(server.Config.Paths.BaseConfig, []byte("type: graph\n"), 0600)) + name := "validate_base" + + var body api.ValidateDAGSpec200JSONResponse + server.Client().Post("/api/v1/dags/validate", api.ValidateDAGSpecJSONRequestBody{ + Name: &name, + Spec: "steps:\n - name: build\n run: echo b\n - name: test\n run: echo t\n depends: [build]\n", + }).ExpectStatus(http.StatusOK).Send(t).Unmarshal(t, &body) + + require.True(t, body.Valid) + require.NotNil(t, body.ReferenceScope) + require.Len(t, body.ReferenceScope.Steps, 2) + assert.Equal(t, []string{"build"}, body.ReferenceScope.Steps[1].Upstream) +} + +func TestDAGSpecSha256(t *testing.T) { + server := test.SetupServer(t) + spec := "steps:\n - run: echo hash\n" + server.Client().Post("/api/v1/dags", api.CreateNewDAGJSONRequestBody{ + Name: "sha_get", Spec: &spec, + }).ExpectStatus(http.StatusCreated).Send(t) + + var body api.GetDAGSpec200JSONResponse + server.Client().Get("/api/v1/dags/sha_get/spec"). + ExpectStatus(http.StatusOK).Send(t).Unmarshal(t, &body) + + sum := sha256.Sum256([]byte(body.Spec)) + assert.Equal(t, hex.EncodeToString(sum[:]), body.Sha256) + assert.Equal(t, spec, body.Spec) +} + +func TestUpdateDAGSpecSha256(t *testing.T) { + server := test.SetupServer(t) + spec := "steps:\n - run: echo one\n" + server.Client().Post("/api/v1/dags", api.CreateNewDAGJSONRequestBody{ + Name: "sha_put", Spec: &spec, + }).ExpectStatus(http.StatusCreated).Send(t) + var got api.GetDAGSpec200JSONResponse + server.Client().Get("/api/v1/dags/sha_put/spec"). + ExpectStatus(http.StatusOK).Send(t).Unmarshal(t, &got) + + next := "steps:\n - run: echo two\n" + var put api.UpdateDAGSpec200JSONResponse + server.Client().Put("/api/v1/dags/sha_put/spec", api.UpdateDAGSpecJSONRequestBody{ + Spec: next, ExpectedSha256: &got.Sha256, + }).ExpectStatus(http.StatusOK).Send(t).Unmarshal(t, &put) + sum := sha256.Sum256([]byte(next)) + assert.Equal(t, hex.EncodeToString(sum[:]), put.Sha256) + + // The returned hash lets the client save again without a re-read. + server.Client().Put("/api/v1/dags/sha_put/spec", api.UpdateDAGSpecJSONRequestBody{ + Spec: spec, ExpectedSha256: &put.Sha256, + }).ExpectStatus(http.StatusOK).Send(t) +} + +func TestUpdateDAGSpecConflict(t *testing.T) { + server := test.SetupServer(t) + spec := "steps:\n - run: echo one\n" + server.Client().Post("/api/v1/dags", api.CreateNewDAGJSONRequestBody{ + Name: "sha_conflict", Spec: &spec, + }).ExpectStatus(http.StatusCreated).Send(t) + + wrong := strings.Repeat("0", 64) + var apiErr api.Error + server.Client().Put("/api/v1/dags/sha_conflict/spec", api.UpdateDAGSpecJSONRequestBody{ + Spec: "steps:\n - run: echo two\n", ExpectedSha256: &wrong, + }).ExpectStatus(http.StatusConflict).Send(t).Unmarshal(t, &apiErr) + assert.Equal(t, api.ErrorCodeConflict, apiErr.Code) + assert.NotEmpty(t, apiErr.Message) + + var got api.GetDAGSpec200JSONResponse + server.Client().Get("/api/v1/dags/sha_conflict/spec"). + ExpectStatus(http.StatusOK).Send(t).Unmarshal(t, &got) + assert.Equal(t, spec, got.Spec) +} diff --git a/internal/service/frontend/api/v1/editor_issues.go b/internal/service/frontend/api/v1/editor_issues.go new file mode 100644 index 0000000000..000623aa06 --- /dev/null +++ b/internal/service/frontend/api/v1/editor_issues.go @@ -0,0 +1,731 @@ +// Copyright (C) 2026 Yota Hamada +// SPDX-License-Identifier: GPL-3.0-or-later + +package api + +import ( + "bytes" + "context" + "errors" + "fmt" + "reflect" + "regexp" + "slices" + "sort" + "strconv" + "strings" + + "github.com/dagucloud/dagu/v2/api/v1" + cmnvalue "github.com/dagucloud/dagu/v2/internal/cmn/value" + "github.com/dagucloud/dagu/v2/internal/cmn/yamlutil" + "github.com/dagucloud/dagu/v2/internal/ir" + "github.com/dagucloud/dagu/v2/internal/spec" + "github.com/dagucloud/dagu/v2/internal/workspace" + "github.com/go-viper/mapstructure/v2" + "github.com/goccy/go-yaml" +) + +// positionPrefix matches the "[line:column]" prefix on the first line of a load error. +var positionPrefix = regexp.MustCompile(`^\[(\d+):(\d+)\]`) + +// buildValidationIssues converts the results of one spec load into structured issues. +// An error issue keeps the text of the matching errors entry. The order is: load errors, +// build errors, warnings, notices. +func buildValidationIssues(loadErr error, dag *ir.DAG, notices []cmnvalue.ValueReferenceNotice) []api.ValidationIssue { + issues := []api.ValidationIssue{} + scope := newIssueScope(dag) + + if loadErr != nil { + if list, ok := errors.AsType[ir.ErrorList](loadErr); ok { + for _, e := range list { + issues = append(issues, newErrorIssue(api.ValidationIssueSourceLoad, e, scope)) + } + } else { + issues = append(issues, newErrorIssue(api.ValidationIssueSourceLoad, loadErr, scope)) + } + } + if dag != nil { + for _, e := range dag.BuildErrors { + issues = append(issues, newErrorIssue(api.ValidationIssueSourceBuild, e, scope)) + } + for _, w := range dag.BuildWarnings { + issues = append(issues, api.ValidationIssue{ + Severity: api.ValidationIssueSeverityWarning, + Source: api.ValidationIssueSourceWarning, + Message: w, + }) + } + } + for _, n := range notices { + issue := api.ValidationIssue{ + Severity: api.ValidationIssueSeverityNotice, + Source: api.ValidationIssueSourceValueReference, + Message: n.Message, + } + if n.FieldPath != "" { + issue.FieldPath = ptrOf(n.FieldPath) + if prefix := stepPathPrefix.FindString(n.FieldPath); prefix != "" { + issue.StepPath = ptrOf(prefix) + if i := slices.IndexFunc(scope.index, func(e stepEntry) bool { return e.path == prefix }); i >= 0 { + issue.StepName = ptrOf(scope.index[i].name) + } + } + } + if n.Reason != "" { + issue.Reason = ptrOf(string(n.Reason)) + } + issues = append(issues, issue) + } + return issues +} + +// errorPosition returns the 1-based position of an error in the YAML. A YAML +// parser error gives its token. Other errors give the "[line:column]" prefix +// of the message when they have one. +func errorPosition(err error) (line, column int, ok bool) { + if yamlErr, ok := errors.AsType[yaml.Error](err); ok { + if tk := yamlErr.GetToken(); tk != nil && tk.Position != nil && tk.Position.Line > 0 { + return tk.Position.Line, max(tk.Position.Column, 1), true + } + } + if m := positionPrefix.FindStringSubmatch(err.Error()); m != nil { + line, _ := strconv.Atoi(m[1]) + column, _ := strconv.Atoi(m[2]) + return line, max(column, 1), true + } + return 0, 0, false +} + +func newErrorIssue(source api.ValidationIssueSource, err error, scope *issueScope) api.ValidationIssue { + issue := api.ValidationIssue{ + Severity: api.ValidationIssueSeverityError, + Source: source, + Message: err.Error(), + } + if line, column, ok := errorPosition(err); ok { + issue.Line = &line + issue.Column = &column + } + attr := scope.attribute(err) + if attr.stepName != "" { + issue.StepName = ptrOf(attr.stepName) + } + if attr.stepPath != "" { + issue.StepPath = ptrOf(attr.stepPath) + } + if attr.fieldPath != "" { + issue.FieldPath = ptrOf(attr.fieldPath) + } + return issue +} + +// attribution is the location that one error points to. +type attribution struct { + stepName string + stepPath string + fieldPath string +} + +// issueScope holds what the attribution of the errors of one DAG shares. +type issueScope struct { + index stepIndex + yaml []byte + raw *rawDoc + // rawLoaded is true when raw holds the result of the YAML read. + rawLoaded bool +} + +func newIssueScope(dag *ir.DAG) *issueScope { + scope := &issueScope{index: newStepIndex(dag)} + if dag != nil { + scope.yaml = dag.YamlData + } + return scope +} + +// rawDoc returns the raw main document, or nil when the DAG has none. The +// YAML is read once, when the first error needs it. +func (s *issueScope) rawDoc() *rawDoc { + if !s.rawLoaded { + s.rawLoaded = true + s.raw = parseRawDoc(s.yaml) + } + return s.raw +} + +// stepLocators find the step of an error, from the most to the least reliable. +var stepLocators = []func(errorPath, stepIndex) (stepEntry, bool){ + stepFromValue, + func(p errorPath, _ stepIndex) (stepEntry, bool) { return handlerFromDecodeName(p) }, + stepFromFieldPrefix, + stepFromQuotedText, + stepFromOwnerText, +} + +// attribute finds the step and the field that err is about. It tries each path +// of the error tree in order and uses the first path that gives a step. When +// the DAG has no such step, because the build stopped at it, the raw YAML +// gives the step. A failed lookup gives the zero value. +func (s *issueScope) attribute(err error) attribution { + paths := errorPaths(err) + var partial *attribution + for _, path := range paths { + for _, locate := range stepLocators { + entry, ok := locate(path, s.index) + if !ok { + continue + } + if entry.path != "" { + return attribution{ + stepName: entry.name, + stepPath: entry.path, + fieldPath: fieldPathOf(path, entry.path), + } + } + if partial == nil { + partial = &attribution{stepName: entry.name, fieldPath: fieldPathOf(path, "")} + // The name is known, so the raw document gives the path. + if raw := s.rawDoc(); raw != nil { + if r, ok := raw.stepByName(entry.name); ok { + partial.stepPath = r.path + partial.fieldPath = fieldPathOf(path, r.path) + } + } + } + } + } + if partial != nil { + return *partial + } + for _, path := range paths { + if raw := s.rawDoc(); raw != nil { + if r, ok := raw.locate(path); ok { + return attribution{stepName: r.name, stepPath: r.path, fieldPath: fieldPathOf(path, r.path)} + } + } + } + if len(paths) > 0 { + return attribution{fieldPath: fieldPathOf(paths[0], "")} + } + return attribution{} +} + +// stepWrapperFields are the fields that only wrap the error of a step or a handler. +var stepWrapperFields = []string{"steps", "handlers", "handler_on"} + +// dagFields are the first segments of fields that always belong to the DAG, not to a step. +var dagFields = []string{ + "params", "consts", "defaults", "registry_auths", "harnesses", "otel", + "tasks", "schedule", "dependencies", "step_types", "actions", +} + +// fieldPathOf builds the field path of an error from its ValidationError +// chain. stepPath is the path of the step that owns the error, or empty. It +// returns an empty string when the field cannot be placed in the DAG. +func fieldPathOf(path errorPath, stepPath string) string { + if len(path.fields) == 0 && decodeHandlerPrefix.MatchString(path.decodeName) { + return path.decodeName + } + rest := path.fields + wrapped := false + for len(rest) > 0 && slices.Contains(stepWrapperFields, rest[0]) { + rest = rest[1:] + wrapped = true + } + if len(rest) > 0 && stepPathPrefix.MatchString(rest[0]) { + return rest[0] + } + switch { + case stepPath != "": + joined := strings.Join(append([]string{stepPath}, rest...), ".") + if strings.HasSuffix(joined, ".with") { + if suffix := schemaFieldSuffix(path.detail); suffix != "" { + joined += "." + suffix + } + } + return joined + case wrapped || len(rest) == 0: + return "" + } + first, _, _ := strings.Cut(rest[0], ".") + if !slices.Contains(dagFields, first) { + return "" + } + return strings.Join(rest, ".") +} + +// schemaLocation matches a schema location in the message of a JSON-schema +// error, such as "validating /properties/repeat:". +var schemaLocation = regexp.MustCompile(`validating (/[^\s:]+):`) + +// extraProperty matches the message about one unexpected input property. +var extraProperty = regexp.MustCompile(`unexpected additional properties \["([^"]+)"\]`) + +// schemaFieldSuffix returns the input field that a JSON-schema error is about, +// relative to the "with" of the step. The validator reports the location in +// the schema, not in the input, so only a chain of "properties" gives a +// field. One unexpected property gives its name. Other errors give "". +func schemaFieldSuffix(detail string) string { + if locations := schemaLocation.FindAllStringSubmatch(detail, -1); len(locations) > 0 { + var names []string + tokens := strings.Split(strings.TrimPrefix(locations[len(locations)-1][1], "/"), "/") + for i := 0; i+1 < len(tokens) && tokens[i] == "properties"; i += 2 { + names = append(names, tokens[i+1]) + } + // Anything else in the location leaves the chain, so the field is unknown. + if len(names)*2 == len(tokens) { + return strings.Join(names, ".") + } + return "" + } + if m := extraProperty.FindStringSubmatch(detail); m != nil { + return m[1] + } + return "" +} + +// stepPathPrefix matches the step that a field path starts with. +var stepPathPrefix = regexp.MustCompile(`^(?:steps\[\d+\]|handler_on\.[a-z]+)`) + +// stepFromFieldPrefix reads the step from a field path such as +// "steps[1].run[0].command". The inferred dependency check reports its errors +// with such paths. +func stepFromFieldPrefix(path errorPath, index stepIndex) (stepEntry, bool) { + for _, field := range path.fields { + prefix := stepPathPrefix.FindString(field) + if prefix == "" { + continue + } + for _, e := range index { + if e.path == prefix { + return e, true + } + } + } + return stepEntry{}, false +} + +// quotedStepText matches the owner of a message such as step 'b': ... , +// step "b": ... or router "r" references ... . +var quotedStepText = regexp.MustCompile(`\b(?:step|router) (?:'([^']+)'|("(?:[^"\\]|\\.)*"))`) + +// stepFromQuotedText reads a quoted step name from the message. The entry has +// no path when the DAG has no step of this name, for example when the build +// stopped before it produced steps. +func stepFromQuotedText(path errorPath, index stepIndex) (stepEntry, bool) { + m := quotedStepText.FindStringSubmatch(path.detail) + if m == nil { + return stepEntry{}, false + } + name := m[1] + if m[2] != "" { + unquoted, err := strconv.Unquote(m[2]) + if err != nil { + return stepEntry{}, false + } + name = unquoted + } + return index.byName(name), name != "" +} + +// stepWord matches the word "step" that starts a clause of a message. +var stepWord = regexp.MustCompile(`(?:^|\s)step `) + +// stepFromOwnerText reads the owner of a message such as "step b depends on +// non-existent step zz". The name is not quoted, so the first "step " is +// followed by the longest step name of the DAG. Names that the DAG does not +// have give no result. +func stepFromOwnerText(path errorPath, index stepIndex) (stepEntry, bool) { + loc := stepWord.FindStringIndex(path.detail) + if loc == nil { + return stepEntry{}, false + } + rest := path.detail[loc[1]:] + best := -1 + for i, e := range index { + if e.name != "" && strings.HasPrefix(rest, e.name+" ") && (best < 0 || len(e.name) > len(index[best].name)) { + best = i + } + } + if best < 0 { + return stepEntry{}, false + } + return index[best], true +} + +// byName returns the first step of this name, or an entry that holds only the name. +func (x stepIndex) byName(name string) stepEntry { + for _, e := range x { + if e.name == name { + return e + } + } + return stepEntry{name: name} +} + +// stepFromValue resolves the Value of a "steps" or "handler_on" validation +// error to a step. +func stepFromValue(path errorPath, index stepIndex) (stepEntry, bool) { + i := path.stepsFieldIndex() + if i < 0 { + return stepEntry{}, false + } + value, _ := path.values[i].(string) + if value == "" { + return stepEntry{}, false + } + return index.lookupValue(value, path.detail) +} + +// stepEntry is one step of the DAG as the editor keys it. +type stepEntry struct { + name string + id string + // path is "steps[]" or "handler_on.". + path string +} + +// stepIndex lists the steps of a DAG in editor order. +type stepIndex []stepEntry + +func newStepIndex(dag *ir.DAG) stepIndex { + if dag == nil { + return nil + } + index := make(stepIndex, 0, len(dag.Steps)+len(handlerEvents)) + for i := range dag.Steps { + index = append(index, stepEntry{ + name: dag.Steps[i].Name, + id: dag.Steps[i].ID, + path: fmt.Sprintf("steps[%d]", i), + }) + } + for _, h := range handlerEvents { + if step := h.step(&dag.HandlerOn); step != nil { + index = append(index, stepEntry{name: step.Name, id: step.ID, path: "handler_on." + h.event}) + } + } + return index +} + +// handlerEvents lists the lifecycle handlers in the order of the editor. +var handlerEvents = []struct { + event string + step func(*ir.HandlerOn) *ir.Step +}{ + {"init", func(h *ir.HandlerOn) *ir.Step { return h.Init }}, + {"success", func(h *ir.HandlerOn) *ir.Step { return h.Success }}, + {"failure", func(h *ir.HandlerOn) *ir.Step { return h.Failure }}, + {"abort", func(h *ir.HandlerOn) *ir.Step { return h.Abort }}, + {"exit", func(h *ir.HandlerOn) *ir.Step { return h.Exit }}, + {"wait", func(h *ir.HandlerOn) *ir.Step { return h.Wait }}, +} + +// lookupValue resolves the value of a "steps" validation error. The value is a +// step name or a step ID. The message tells which one: "step ID" before +// "step name" means an ID. Otherwise the name has priority. A duplicate error +// points to the later step. +func (x stepIndex) lookupValue(value, detail string) (stepEntry, bool) { + byName := func(e stepEntry) bool { return e.name == value } + byID := func(e stepEntry) bool { return e.id == value } + first, second := byName, byID + if i, j := strings.Index(detail, "step ID"), strings.Index(detail, "step name"); i >= 0 && (j < 0 || i < j) { + first, second = byID, byName + } + duplicate := strings.Contains(detail, "duplicate") || strings.Contains(detail, "must be unique") + for _, match := range []func(stepEntry) bool{first, second} { + var found []stepEntry + for _, e := range x { + if match(e) { + found = append(found, e) + } + } + switch { + case len(found) == 0: + case duplicate: + return found[len(found)-1], true + default: + return found[0], true + } + } + return stepEntry{}, false +} + +// maxErrorPaths bounds the paths that errorPaths returns for one error. +const maxErrorPaths = 64 + +// errorPath is one root-to-leaf path through an error tree. +type errorPath struct { + // fields and values hold the Field and Value of each ir.ValidationError on + // the path, from the outermost to the innermost. + fields []string + values []any + // decodeName is the name that the first mapstructure error of the path + // gives to the failed key, such as "[s2].Depends" or "handler_on.failure". + decodeName string + // detail is the message of the first error below the innermost + // ir.ValidationError that does not wrap several errors. + detail string +} + +// stepsFieldIndex returns the index of the innermost "steps" or "handler_on" +// field that has a value, or -1. +func (p errorPath) stepsFieldIndex() int { + for i, v := range slices.Backward(p.fields) { + if (v == "steps" || v == "handler_on") && p.values[i] != nil { + return i + } + } + return -1 +} + +// errorPaths walks err depth-first through Unwrap() error and Unwrap() []error +// and returns one path for each leaf, in tree order. +func errorPaths(err error) []errorPath { + var paths []errorPath + var walk func(e error, cur errorPath, hasDetail bool) + walk = func(e error, cur errorPath, hasDetail bool) { + if e == nil || len(paths) >= maxErrorPaths { + return + } + if de, ok := e.(*mapstructure.DecodeError); ok && cur.decodeName == "" { + cur.decodeName = de.Name() + } + switch x := e.(type) { + case *ir.ValidationError: + cur.fields = append(slices.Clip(cur.fields), x.Field) + cur.values = append(slices.Clip(cur.values), x.Value) + cur.detail, hasDetail = "", false + case ir.ErrorList, interface{ Unwrap() []error }: + default: + if !hasDetail { + cur.detail, hasDetail = e.Error(), true + } + } + var children []error + switch x := e.(type) { + case interface{ Unwrap() []error }: + children = x.Unwrap() + case interface{ Unwrap() error }: + if c := x.Unwrap(); c != nil { + children = []error{c} + } + } + children = slices.DeleteFunc(slices.Clone(children), func(c error) bool { return c == nil }) + if len(children) == 0 { + if !hasDetail { + cur.detail = e.Error() + } + paths = append(paths, cur) + return + } + for _, c := range children { + walk(c, cur, hasDetail) + } + } + walk(err, errorPath{}, false) + return paths +} + +// specValueReferenceNotices returns the notices of one load of source, with the same +// options as GetDAGSpec. LoadSpec does not return the notices that the build reports, +// so the spec is loaded once more. A failed load gives no notices. +func (a *API) specValueReferenceNotices(ctx context.Context, name, source string) []cmnvalue.ValueReferenceNotice { + opts := []spec.LoadOption{ + spec.WithName(name), + spec.WithAllowBuildErrors(), + spec.WithoutEval(), + spec.WithWorkspaceBaseConfigDir(workspace.BaseConfigDir(a.config.Paths.DAGsDir)), + } + if a.config.Paths.BaseConfig != "" { + opts = append(opts, spec.WithBaseConfig(a.config.Paths.BaseConfig)) + } + result, err := spec.LoadYAMLWithResult(ctx, []byte(source), opts...) + if err != nil || result == nil { + return nil + } + return result.ValueReferenceNotices +} + +// rawStep is one step of the raw YAML. The path is the one that the spec +// gives to the step: its place in the flat step list of the DAG. +type rawStep struct { + path string + // name is the explicit name, or the key of the map form. It is empty when + // the spec generates the name. + name string + entry map[string]any +} + +// rawDoc is the main YAML document, read the way internal/spec reads it. +type rawDoc struct { + steps []rawStep + handlers []rawStep +} + +// parseRawDoc reads the first non-empty document of data. It returns nil when +// the data has no usable document. +func parseRawDoc(data []byte) *rawDoc { + if len(data) == 0 { + return nil + } + decoder := yaml.NewDecoder(bytes.NewReader(yamlutil.ClearEmptyDocumentSeparators(data))) + var doc map[string]any + for len(doc) == 0 { + if err := decoder.Decode(&doc); err != nil { + return nil + } + } + raw := &rawDoc{steps: flattenRawSteps(doc["steps"])} + handlers, _ := doc["handler_on"].(map[string]any) + for _, h := range handlerEvents { + if entry, ok := handlers[h.event].(map[string]any); ok { + raw.handlers = append(raw.handlers, rawStep{ + path: "handler_on." + h.event, + name: handlerName(h.event), + entry: entry, + }) + } + } + return raw +} + +// flattenRawSteps lists the steps the way the spec builds them. The array +// form keeps the order and expands nested arrays. The map form sorts by key. +// The list stops at the first entry that is not a step. +func flattenRawSteps(node any) []rawStep { + var steps []rawStep + add := func(name string, entry map[string]any) { + steps = append(steps, rawStep{path: fmt.Sprintf("steps[%d]", len(steps)), name: name, entry: entry}) + } + asEntry := func(item any) (map[string]any, bool) { + switch v := item.(type) { + case string: + return map[string]any{"command": v}, true + case map[string]any: + return v, true + } + return nil, false + } + switch v := node.(type) { + case []any: + for _, item := range v { + group := []any{item} + if nested, ok := item.([]any); ok { + group = nested + } + for _, member := range group { + entry, ok := asEntry(member) + if !ok { + return steps + } + name, _ := entry["name"].(string) + add(name, entry) + } + } + case map[string]any: + keys := make([]string, 0, len(v)) + for key := range v { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + entry, _ := v[key].(map[string]any) + add(key, entry) + } + } + return steps +} + +// stepByName returns the only step with this explicit name. +func (d *rawDoc) stepByName(name string) (rawStep, bool) { + return uniqueRawStep(d.steps, func(r rawStep) bool { return name != "" && r.name == name }) +} + +func uniqueRawStep(list []rawStep, match func(rawStep) bool) (rawStep, bool) { + var found []rawStep + for _, r := range list { + if match(r) { + found = append(found, r) + } + } + if len(found) != 1 { + return rawStep{}, false + } + return found[0], true +} + +// decodeKeyPrefix matches the key of the map form in the name of a decode error. +var decodeKeyPrefix = regexp.MustCompile(`^\[([^\]]+)\]`) + +// decodeHandlerPrefix matches a handler in the name of a decode error. +var decodeHandlerPrefix = regexp.MustCompile(`^handler_on\.([a-z]+)`) + +// handlerFromDecodeName reads the handler from the name of a decode error. The +// decode of the whole document fails before it builds the DAG, so this needs no +// DAG and no raw YAML. +func handlerFromDecodeName(path errorPath) (stepEntry, bool) { + m := decodeHandlerPrefix.FindStringSubmatch(path.decodeName) + if m == nil { + return stepEntry{}, false + } + for _, h := range handlerEvents { + if h.event == m[1] { + return stepEntry{name: handlerName(h.event), path: "handler_on." + h.event}, true + } + } + return stepEntry{}, false +} + +// handlerName returns the name that the spec gives to the handler of an event. +func handlerName(event string) string { + return "on" + strings.ToUpper(event[:1]) + event[1:] +} + +// locate finds the step or the handler that a path of an error is about, from +// the names of decode errors and from the values of validation errors. A +// match must be unique. +func (d *rawDoc) locate(path errorPath) (rawStep, bool) { + if m := decodeKeyPrefix.FindStringSubmatch(path.decodeName); m != nil { + if r, ok := d.stepByName(m[1]); ok { + return r, true + } + } + k := len(path.values) - 1 + for k >= 0 && path.values[k] == nil { + k-- + } + if k < 0 { + return rawStep{}, false + } + value, field := path.values[k], path.fields[k] + match := func(r rawStep) bool { return rawStepHolds(r, value, field) } + switch { + case slices.Contains(path.fields, "handlers"): + return uniqueRawStep(d.handlers, match) + case slices.Contains(path.fields, "steps"): + return uniqueRawStep(d.steps, match) + } + return rawStep{}, false +} + +// rawStepHolds reports whether value is the step, or the value of field in the +// step. A value that has a name belongs to the step of this name. +func rawStepHolds(r rawStep, value any, field string) bool { + if m, ok := value.(map[string]any); ok { + if name, ok := m["name"].(string); ok && name != "" { + return r.name == name + } + } + return sameValue(value, r.entry) || sameValue(value, r.entry[field]) +} + +// sameValue compares a value of a validation error with a raw YAML value. The +// error holds a typed copy, such as []string, of a raw []any, so the check +// falls back to the printed form. +func sameValue(a, b any) bool { + if a == nil || b == nil { + return false + } + return reflect.DeepEqual(a, b) || fmt.Sprint(a) == fmt.Sprint(b) +} diff --git a/internal/service/frontend/api/v1/editor_issues_api_test.go b/internal/service/frontend/api/v1/editor_issues_api_test.go new file mode 100644 index 0000000000..b3cabf84b6 --- /dev/null +++ b/internal/service/frontend/api/v1/editor_issues_api_test.go @@ -0,0 +1,134 @@ +// Copyright (C) 2026 Yota Hamada +// SPDX-License-Identifier: GPL-3.0-or-later + +package api_test + +import ( + "net/http" + "testing" + + "github.com/dagucloud/dagu/v2/api/v1" + "github.com/dagucloud/dagu/v2/internal/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// validateIssues posts spec to the validate endpoint and returns the response. +func validateIssues(t *testing.T, server test.Server, spec string) api.ValidateDAGSpec200JSONResponse { + t.Helper() + name := "issues_api" + var body api.ValidateDAGSpec200JSONResponse + server.Client().Post("/api/v1/dags/validate", api.ValidateDAGSpecJSONRequestBody{ + Name: &name, Spec: spec, + }).ExpectStatus(http.StatusOK).Send(t).Unmarshal(t, &body) + return body +} + +// errorIssues returns the error issues of body and checks that their messages +// are the errors entries, in order. +func errorIssues(t *testing.T, body api.ValidateDAGSpec200JSONResponse) []api.ValidationIssue { + t.Helper() + var issues []api.ValidationIssue + var messages []string + for _, issue := range body.Issues { + if issue.Severity == api.ValidationIssueSeverityError { + issues = append(issues, issue) + messages = append(messages, issue.Message) + } + } + require.NotEmpty(t, issues) + require.Equal(t, body.Errors, messages) + return issues +} + +func TestEditorIssuesBadDepends(t *testing.T) { + server := test.SetupServer(t) + body := validateIssues(t, server, + "steps:\n - name: a\n command: echo a\n - name: b\n command: echo b\n depends: zz\n") + + require.False(t, body.Valid) + issues := errorIssues(t, body) + require.Len(t, issues, 1) + issue := issues[0] + assert.Equal(t, api.ValidationIssueSourceBuild, issue.Source) + require.NotNil(t, issue.StepName) + assert.Equal(t, "b", *issue.StepName) + require.NotNil(t, issue.StepPath) + assert.Equal(t, "steps[1]", *issue.StepPath) + require.NotNil(t, issue.FieldPath) + assert.Equal(t, "steps[1].depends", *issue.FieldPath) +} + +func TestEditorIssuesSyntaxPosition(t *testing.T) { + server := test.SetupServer(t) + body := validateIssues(t, server, "steps:\n - name: a\n command: echo\n x: [\n") + + require.False(t, body.Valid) + issues := errorIssues(t, body) + require.Len(t, issues, 1) + require.NotNil(t, issues[0].Line) + require.NotNil(t, issues[0].Column) + assert.Equal(t, 3, *issues[0].Line) + assert.Equal(t, 4, *issues[0].Column) + assert.Nil(t, issues[0].StepName) +} + +// The build stops at a failing step and returns no steps, so the path comes from the raw YAML. +func TestEditorIssuesBuildStageStep(t *testing.T) { + server := test.SetupServer(t) + body := validateIssues(t, server, + "steps:\n - name: a\n command: echo a\n - name: b\n action: http.request\n with:\n url: 3\n bogus: x\n") + + require.False(t, body.Valid) + issues := errorIssues(t, body) + require.Len(t, issues, 1) + require.NotNil(t, issues[0].StepName) + assert.Equal(t, "b", *issues[0].StepName) + require.NotNil(t, issues[0].StepPath) + assert.Equal(t, "steps[1]", *issues[0].StepPath) + require.NotNil(t, issues[0].FieldPath) + assert.Equal(t, "steps[1].with", *issues[0].FieldPath) +} + +func TestEditorIssuesHandlerStep(t *testing.T) { + server := test.SetupServer(t) + body := validateIssues(t, server, + "steps:\n - name: a\n command: echo a\nhandler_on:\n failure:\n type: bogus\n") + + require.False(t, body.Valid) + issues := errorIssues(t, body) + require.Len(t, issues, 1) + require.NotNil(t, issues[0].StepPath) + assert.Equal(t, "handler_on.failure", *issues[0].StepPath) +} + +func TestEditorIssuesNoticeStep(t *testing.T) { + server := test.SetupServer(t) + body := validateIssues(t, server, + "steps:\n - name: a\n command: echo a\n - name: b\n command: echo ${steps.nope.outputs.x}\n") + + require.True(t, body.Valid) + var found bool + for _, issue := range body.Issues { + if issue.Source != api.ValidationIssueSourceValueReference { + continue + } + found = true + require.NotNil(t, issue.StepPath) + assert.Equal(t, "steps[1]", *issue.StepPath) + require.NotNil(t, issue.StepName) + assert.Equal(t, "b", *issue.StepName) + } + assert.True(t, found) +} + +// One errors entry gives one issue, also when the entry holds a list of errors. +func TestEditorIssuesOnePerEntry(t *testing.T) { + server := test.SetupServer(t) + body := validateIssues(t, server, + "steps:\n - name: a\n command: echo a\n depends: [x1, x2]\n") + + require.False(t, body.Valid) + issues := errorIssues(t, body) + assert.Len(t, issues, len(body.Errors)) +} diff --git a/internal/service/frontend/api/v1/editor_issues_test.go b/internal/service/frontend/api/v1/editor_issues_test.go new file mode 100644 index 0000000000..7d92bc4149 --- /dev/null +++ b/internal/service/frontend/api/v1/editor_issues_test.go @@ -0,0 +1,536 @@ +// Copyright (C) 2026 Yota Hamada +// SPDX-License-Identifier: GPL-3.0-or-later + +package api + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/dagucloud/dagu/v2/api/v1" + cmnvalue "github.com/dagucloud/dagu/v2/internal/cmn/value" + "github.com/dagucloud/dagu/v2/internal/ir" + "github.com/dagucloud/dagu/v2/internal/spec" + "github.com/goccy/go-yaml" + "github.com/goccy/go-yaml/token" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBuildValidationIssuesEmpty(t *testing.T) { + issues := buildValidationIssues(nil, nil, nil) + assert.NotNil(t, issues) + assert.Empty(t, issues) +} + +func TestBuildValidationIssuesPosition(t *testing.T) { + loadErr := ir.ErrorList{ + errors.New("[3:5] bad key\n 1 | a"), + errors.New("no position"), + } + issues := buildValidationIssues(loadErr, nil, nil) + require.Len(t, issues, 2) + + assert.Equal(t, api.ValidationIssueSeverityError, issues[0].Severity) + assert.Equal(t, api.ValidationIssueSourceLoad, issues[0].Source) + assert.Equal(t, loadErr[0].Error(), issues[0].Message) + require.NotNil(t, issues[0].Line) + require.NotNil(t, issues[0].Column) + assert.Equal(t, 3, *issues[0].Line) + assert.Equal(t, 5, *issues[0].Column) + + assert.Nil(t, issues[1].Line) + assert.Nil(t, issues[1].Column) +} + +func TestBuildValidationIssuesStep(t *testing.T) { + dag := &ir.DAG{ + Steps: []ir.Step{{Name: "a"}, {Name: "b"}}, + BuildErrors: []error{ + ir.NewValidationError("steps", "b", errors.New("bad step")), + ir.NewValidationError("params", "x", errors.New("bad param")), + }, + BuildWarnings: []string{"careful"}, + } + issues := buildValidationIssues(nil, dag, nil) + require.Len(t, issues, 3) + + assert.Equal(t, api.ValidationIssueSourceBuild, issues[0].Source) + assert.Equal(t, dag.BuildErrors[0].Error(), issues[0].Message) + require.NotNil(t, issues[0].StepName) + assert.Equal(t, "b", *issues[0].StepName) + require.NotNil(t, issues[0].StepPath) + assert.Equal(t, "steps[1]", *issues[0].StepPath) + + assert.Nil(t, issues[1].StepName) + assert.Nil(t, issues[1].StepPath) + require.NotNil(t, issues[1].FieldPath) + assert.Equal(t, "params", *issues[1].FieldPath) + + assert.Equal(t, api.ValidationIssueSeverityWarning, issues[2].Severity) + assert.Equal(t, api.ValidationIssueSourceWarning, issues[2].Source) + assert.Equal(t, "careful", issues[2].Message) +} + +func TestBuildValidationIssuesNotice(t *testing.T) { + issues := buildValidationIssues(nil, nil, []cmnvalue.ValueReferenceNotice{{ + Message: "unresolved", + FieldPath: "consts.image", + Reason: cmnvalue.ValueReferenceReasonUnknownStepID, + }}) + require.Len(t, issues, 1) + assert.Equal(t, api.ValidationIssueSeverityNotice, issues[0].Severity) + assert.Equal(t, api.ValidationIssueSourceValueReference, issues[0].Source) + assert.Equal(t, "unresolved", issues[0].Message) + require.NotNil(t, issues[0].FieldPath) + assert.Equal(t, "consts.image", *issues[0].FieldPath) + require.NotNil(t, issues[0].Reason) + assert.Equal(t, "unknown_step_id", *issues[0].Reason) +} + +func TestErrorPaths(t *testing.T) { + leaf := errors.New("leaf") + tests := []struct { + name string + err error + want []errorPath + }{ + { + name: "plain", + err: leaf, + want: []errorPath{{detail: "leaf"}}, + }, + { + name: "nested validation errors", + err: ir.NewValidationError("steps", nil, + ir.NewValidationError("with", map[string]any{"a": 1}, leaf)), + want: []errorPath{{ + fields: []string{"steps", "with"}, + values: []any{nil, map[string]any{"a": 1}}, + detail: "leaf", + }}, + }, + { + name: "list inside validation error", + err: ir.NewValidationError("steps", nil, ir.ErrorList{ + ir.NewValidationError("parallel", 3, errors.New("one")), + ir.NewValidationError("foreach", "x", errors.New("two")), + }), + want: []errorPath{ + {fields: []string{"steps", "parallel"}, values: []any{nil, 3}, detail: "one"}, + {fields: []string{"steps", "foreach"}, values: []any{nil, "x"}, detail: "two"}, + }, + }, + { + name: "wrap keeps the outer text", + err: ir.NewValidationError("depends", []string{"a"}, + fmt.Errorf("step 'b': %w", leaf)), + want: []errorPath{{ + fields: []string{"depends"}, + values: []any{[]string{"a"}}, + detail: "step 'b': leaf", + }}, + }, + { + name: "join", + err: errors.Join(errors.New("x"), fmt.Errorf("y: %w", leaf)), + want: []errorPath{{detail: "x"}, {detail: "y: leaf"}}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, errorPaths(tt.err)) + }) + } + assert.Empty(t, errorPaths(nil)) +} + +// loadIssues loads source like the validate endpoint and returns its issues. +func loadIssues(t *testing.T, source string) []api.ValidationIssue { + t.Helper() + dag, err := spec.LoadYAML(context.Background(), []byte(source), + spec.WithName("t"), spec.WithAllowBuildErrors(), spec.WithoutEval()) + issues := buildValidationIssues(err, dag, nil) + for _, issue := range issues { + require.NotEmpty(t, issue.Message) + } + return issues +} + +// wantIssue is the expected location of an issue. An empty field means nil. +type wantIssue struct { + step, stepPath, field string + line, column int +} + +func assertIssue(t *testing.T, want wantIssue, got api.ValidationIssue) { + t.Helper() + str := func(p *string) string { + if p == nil { + return "" + } + return *p + } + num := func(p *int) int { + if p == nil { + return 0 + } + return *p + } + assert.Equal(t, want.step, str(got.StepName), "stepName") + assert.Equal(t, want.stepPath, str(got.StepPath), "stepPath") + assert.Equal(t, want.field, str(got.FieldPath), "fieldPath") + assert.Equal(t, want.line, num(got.Line), "line") + assert.Equal(t, want.column, num(got.Column), "column") +} + +// Each row loads a real spec and checks the location of its first issue. +func TestIssueAttribution(t *testing.T) { + tests := []struct { + name string + source string + count int + issue int // index of the issue to check + want wantIssue + }{ + { + name: "yaml syntax error", + source: "steps:\n - name: a\n command: echo\n x: [\n", + count: 1, + want: wantIssue{line: 3, column: 4}, + }, + { + name: "yaml error inside a wrapped decode error", + source: "steps:\n - name: a\n command: echo\nsteps:\n - name: b\n command: echo\n", + count: 1, + want: wantIssue{line: 4, column: 1}, + }, + { + name: "duplicate step id points to the later step", + source: "steps:\n - name: a\n id: x\n command: echo\n - name: b\n id: x\n command: echo\n", + count: 1, + want: wantIssue{step: "b", stepPath: "steps[1]", field: "steps[1]"}, + }, + { + name: "duplicate step name points to the later step", + source: "steps:\n - name: a\n command: echo\n - name: a\n command: echo\n", + count: 1, + want: wantIssue{step: "a", stepPath: "steps[1]", field: "steps[1]"}, + }, + { + name: "double quoted step name", + source: "type: agent\nsteps:\n - name: a\n command: echo\n depends: [x]\n", + count: 2, + issue: 1, + want: wantIssue{step: "a", stepPath: "steps[0]", field: "steps[0].depends"}, + }, + { + name: "single quoted step name located in the raw YAML", + source: "type: chain\nsteps:\n - name: a\n command: echo\n - name: b\n command: echo\n depends: [a]\n", + count: 1, + want: wantIssue{step: "b", stepPath: "steps[1]", field: "steps[1].depends"}, + }, + { + name: "missing dependency belongs to the owner step", + source: "steps:\n - name: a\n command: echo\n - name: b\n command: echo\n depends: zz\n", + count: 1, + want: wantIssue{step: "b", stepPath: "steps[1]", field: "steps[1].depends"}, + }, + { + name: "longest step name wins", + source: "steps:\n - name: build\n command: echo\n - name: build app\n command: echo\n depends: zz\n", + count: 1, + want: wantIssue{step: "build app", stepPath: "steps[1]", field: "steps[1].depends"}, + }, + { + name: "step in a nested group", + source: "steps:\n - name: a\n command: echo\n - - name: b\n command: echo\n - name: c\n command: echo\n depends: [qq]\n", + count: 1, + want: wantIssue{step: "c", stepPath: "steps[2]", field: "steps[2].depends"}, + }, + { + name: "step of the map form", + source: "steps:\n s1:\n command: echo\n s2:\n command: echo\n depends: [qq]\n", + count: 1, + want: wantIssue{step: "s2", stepPath: "steps[1]", field: "steps[1].depends"}, + }, + { + name: "generated step name", + source: "steps:\n - command: echo\n depends: [x]\n", + count: 1, + want: wantIssue{step: "cmd_1", stepPath: "steps[0]", field: "steps[0].depends"}, + }, + { + name: "router target that does not exist", + source: "steps:\n - name: r\n type: router\n value: x\n routes:\n a: [nope]\n", + count: 1, + want: wantIssue{step: "r", stepPath: "steps[0]", field: "steps[0].routes"}, + }, + { + name: "router in a chain", + source: "type: chain\nsteps:\n - name: r\n type: router\n value: x\n routes:\n a: [s]\n - name: s\n command: echo\n", + count: 1, + want: wantIssue{step: "r", stepPath: "steps[0]", field: "steps[0].type"}, + }, + { + name: "inferred dependency cycle", + source: "type: graph\nsteps:\n - name: a\n id: ida\n command: echo ${steps.idb.outputs.x}\n" + + " - name: b\n id: idb\n command: echo ${steps.ida.outputs.x}\n", + count: 1, // one errors entry holds both fields + want: wantIssue{step: "b", stepPath: "steps[1]", field: "steps[1].run[0].cmd_with_args"}, + }, + { + name: "unknown key of a step", + source: "steps:\n - name: a\n command: echo\n - name: b\n runx: echo\n", + count: 1, + want: wantIssue{step: "b", stepPath: "steps[1]", field: "steps[1]"}, + }, + { + name: "invalid with of a step", + source: "steps:\n - name: a\n command: echo\n - name: b\n action: http.request\n with:\n url: 3\n bogus: x\n", + count: 1, + want: wantIssue{step: "b", stepPath: "steps[1]", field: "steps[1].with"}, + }, + { + name: "input of a custom action", + source: "step_types:\n greet:\n type: command\n input_schema:\n type: object\n additionalProperties: false\n" + + " properties:\n repeat:\n type: integer\n template:\n command: echo hi\n" + + "steps:\n - name: x\n command: echo\n - name: g\n type: greet\n with:\n repeat: abc\n", + count: 1, + want: wantIssue{step: "g", stepPath: "steps[1]", field: "steps[1].with.repeat"}, + }, + { + name: "unexpected input of a custom action", + source: "step_types:\n greet:\n type: command\n input_schema:\n type: object\n additionalProperties: false\n" + + " properties:\n message:\n type: string\n template:\n command: echo hi\n" + + "steps:\n - name: g\n type: greet\n with:\n bogus: abc\n", + count: 1, + want: wantIssue{step: "g", stepPath: "steps[0]", field: "steps[0].with.bogus"}, + }, + { + name: "decode error in the map form", + source: "steps:\n s1:\n command: echo\n s2:\n runx: echo\n", + count: 1, + want: wantIssue{step: "s2", stepPath: "steps[1]", field: "steps[1]"}, + }, + { + name: "decode error of a field in the map form", + source: "steps:\n s1:\n command: echo\n s2:\n depends: 3\n command: echo\n", + count: 1, + want: wantIssue{step: "s2", stepPath: "steps[1]", field: "steps[1]"}, + }, + { + name: "invalid handler found by its value", + source: "steps:\n - name: a\n command: echo\nhandler_on:\n failure:\n type: bogus\n", + count: 1, + want: wantIssue{step: "onFailure", stepPath: "handler_on.failure", field: "handler_on.failure.type"}, + }, + { + name: "unknown key of a handler", + source: "steps:\n - name: a\n command: echo\nhandler_on:\n failure:\n runx: 1\n", + count: 1, + want: wantIssue{step: "onFailure", stepPath: "handler_on.failure", field: "handler_on.failure"}, + }, + { + name: "handler by its name", + source: "type: build\nworking_dir: .\nhandler_on:\n success:\n id: cleanup\n run: echo cleanup\n" + + " inputs:\n - name: source\n path: source.txt\nsteps:\n - name: build\n run: echo build\n", + count: 1, + want: wantIssue{step: "onSuccess", stepPath: "handler_on.success", field: "handler_on.success"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + issues := loadIssues(t, tt.source) + require.Len(t, issues, tt.count) + assertIssue(t, tt.want, issues[tt.issue]) + }) + } +} + +func TestIssueHandlerPaths(t *testing.T) { + handler := func(name string) *ir.Step { return &ir.Step{Name: name, ID: "id_" + name} } + dag := &ir.DAG{ + Steps: []ir.Step{{Name: "a"}}, + HandlerOn: ir.HandlerOn{ + Init: handler("onInit"), + Success: handler("onSuccess"), + Failure: handler("onFailure"), + Abort: handler("onAbort"), + Exit: handler("onExit"), + Wait: handler("onWait"), + }, + } + for event, name := range map[string]string{ + "init": "onInit", "success": "onSuccess", "failure": "onFailure", + "abort": "onAbort", "exit": "onExit", "wait": "onWait", + } { + path := "handler_on." + event + want := wantIssue{step: name, stepPath: path, field: path} + byName := ir.NewValidationError("handler_on", name, errors.New("bad")) + byID := ir.NewValidationError("steps", "id_"+name, errors.New("bad")) + byText := fmt.Errorf("step '%s': bad", name) + byField := ir.NewValidationError("handler_on."+event+".type", nil, errors.New("bad")) + for _, err := range []error{byName, byID, byText, byField} { + issues := buildValidationIssues(nil, &ir.DAG{ + Steps: dag.Steps, HandlerOn: dag.HandlerOn, BuildErrors: []error{err}, + }, nil) + require.Len(t, issues, 1) + want := want + if err == byField { + want.field = path + ".type" + } + assertIssue(t, want, issues[0]) + } + } +} + +func TestFieldPathOf(t *testing.T) { + tests := []struct { + name string + fields []string + stepPath string + want string + }{ + {"dag field", []string{"params"}, "", "params"}, + {"dag field with segments", []string{"consts.image"}, "", "consts.image"}, + {"step field", []string{"steps", "with"}, "steps[2]", "steps[2].with"}, + {"dotted step field", []string{"steps", "retry_policy.interval_sec"}, "steps[0]", "steps[0].retry_policy.interval_sec"}, + {"wrappers only", []string{"steps", "steps"}, "steps[1]", "steps[1]"}, + {"handler field", []string{"handlers", "type"}, "handler_on.failure", "handler_on.failure.type"}, + {"absolute path", []string{"steps[1].run[0].command"}, "steps[1]", "steps[1].run[0].command"}, + {"step field of an unknown step", []string{"steps", "with"}, "", ""}, + {"ambiguous field of an unknown step", []string{"container"}, "", ""}, + {"no field", nil, "", ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := fieldPathOf(errorPath{fields: tt.fields, values: make([]any, len(tt.fields))}, tt.stepPath) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestRawDoc(t *testing.T) { + t.Run("array form with groups and shorthand", func(t *testing.T) { + raw := parseRawDoc([]byte("steps:\n - echo hi\n - - name: b\n command: x\n - name: c\n command: y\n - name: d\n command: z\n")) + require.NotNil(t, raw) + var got []string + for _, r := range raw.steps { + got = append(got, r.path+"="+r.name) + } + assert.Equal(t, []string{"steps[0]=", "steps[1]=b", "steps[2]=c", "steps[3]=d"}, got) + }) + t.Run("map form sorts by key", func(t *testing.T) { + raw := parseRawDoc([]byte("steps:\n zeta:\n command: x\n alpha:\n command: y\n")) + require.NotNil(t, raw) + require.Len(t, raw.steps, 2) + assert.Equal(t, "alpha", raw.steps[0].name) + assert.Equal(t, "steps[1]", raw.steps[1].path) + }) + t.Run("only the first document", func(t *testing.T) { + raw := parseRawDoc([]byte("steps:\n - name: a\n command: x\n---\nname: sub\nsteps:\n - name: z\n command: y\n")) + require.NotNil(t, raw) + require.Len(t, raw.steps, 1) + assert.Equal(t, "a", raw.steps[0].name) + }) + t.Run("no usable document", func(t *testing.T) { + assert.Nil(t, parseRawDoc(nil)) + assert.Nil(t, parseRawDoc([]byte("steps: [\n"))) + }) + t.Run("equal steps do not match", func(t *testing.T) { + raw := parseRawDoc([]byte("steps:\n - command: x\n - command: x\n")) + require.NotNil(t, raw) + path := errorPath{fields: []string{"steps"}, values: []any{map[string]any{"command": "x"}}} + _, ok := raw.locate(path) + assert.False(t, ok) + }) +} + +// A DAG without YAML data and without steps still gives the issue. +func TestIssueWithoutYAMLData(t *testing.T) { + dag := &ir.DAG{BuildErrors: []error{ + ir.NewValidationError("steps", nil, ir.NewValidationError("with", map[string]any{"a": 1}, errors.New("bad"))), + fmt.Errorf("step 'ghost': bad"), + }} + issues := buildValidationIssues(nil, dag, nil) + require.Len(t, issues, 2) + assertIssue(t, wantIssue{}, issues[0]) + assertIssue(t, wantIssue{step: "ghost"}, issues[1]) +} + +// The token of a parser error gives the position even when the message has no prefix. +func TestErrorPositionFromToken(t *testing.T) { + err := fmt.Errorf("document 0: %w", &yaml.SyntaxError{ + Message: "bad", + Token: &token.Token{Position: &token.Position{Line: 7, Column: 0}}, + }) + line, column, ok := errorPosition(err) + require.True(t, ok) + assert.Equal(t, 7, line) + assert.Equal(t, 1, column) + + _, _, ok = errorPosition(errors.New("no position")) + assert.False(t, ok) +} + +func TestSchemaFieldSuffix(t *testing.T) { + tests := []struct { + name, detail, want string + }{ + {"property", `invalid "g" input: validating root: validating /properties/repeat: type: abc`, "repeat"}, + {"nested properties", "validating root: validating /properties/a/properties/b: type", "a.b"}, + {"item of a list", "validating root: validating /properties/a/items: type", ""}, + {"additional property", `validating root: unexpected additional properties ["bogus"]`, "bogus"}, + {"several additional properties", `validating root: unexpected additional properties ["a" "b"]`, ""}, + {"root", "validating root: missing properties", ""}, + {"other", "something else", ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, schemaFieldSuffix(tt.detail)) + }) + } +} + +// A notice of a step field gets the step path of its field path. +func TestIssueNoticeStep(t *testing.T) { + tests := []struct { + name string + source string + want wantIssue + }{ + { + name: "step field", + source: "steps:\n - name: a\n command: echo\n - name: b\n command: echo ${steps.nope.outputs.x}\n", + want: wantIssue{step: "b", stepPath: "steps[1]"}, + }, + { + name: "handler field", + source: "steps:\n - name: a\n command: echo\nhandler_on:\n failure:\n command: echo ${steps.nope.outputs.x}\n", + want: wantIssue{step: "onFailure", stepPath: "handler_on.failure"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := spec.LoadYAMLWithResult(context.Background(), []byte(tt.source), + spec.WithName("t"), spec.WithAllowBuildErrors(), spec.WithoutEval()) + require.NoError(t, err) + require.NotEmpty(t, result.ValueReferenceNotices) + issues := buildValidationIssues(nil, result.DAG, result.ValueReferenceNotices) + require.Len(t, issues, 1) + require.NotNil(t, issues[0].FieldPath) + assert.Contains(t, *issues[0].FieldPath, tt.want.stepPath) + tt.want.field = *issues[0].FieldPath + assertIssue(t, tt.want, issues[0]) + }) + } + + t.Run("dag field has no step", func(t *testing.T) { + issues := buildValidationIssues(nil, nil, []cmnvalue.ValueReferenceNotice{{Message: "m", FieldPath: "consts.image"}}) + require.Len(t, issues, 1) + assertIssue(t, wantIssue{field: "consts.image"}, issues[0]) + }) +} diff --git a/internal/service/frontend/api/v1/editor_scope.go b/internal/service/frontend/api/v1/editor_scope.go new file mode 100644 index 0000000000..4e1369f916 --- /dev/null +++ b/internal/service/frontend/api/v1/editor_scope.go @@ -0,0 +1,152 @@ +// Copyright (C) 2026 Yota Hamada +// SPDX-License-Identifier: GPL-3.0-or-later + +package api + +import ( + "fmt" + "reflect" + "regexp" + "sort" + "strings" + + "github.com/dagucloud/dagu/v2/api/v1" + "github.com/dagucloud/dagu/v2/internal/ir" +) + +// envNamePattern is the env name rule of specs/003-value-resolution.md. +var envNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) + +// buildReferenceScope lists the values that steps of dag can reference. +// Every array is non-nil. It returns nil when dag is nil. +func buildReferenceScope(dag *ir.DAG) *api.ReferenceScope { + if dag == nil { + return nil + } + return &api.ReferenceScope{ + Params: scopeParams(dag), + Consts: scopeConsts(dag), + Env: scopeEnv(dag), + Secrets: scopeSecrets(dag), + Steps: scopeSteps(dag), + } +} + +// scopeParams lists the named params of dag in definition order. +func scopeParams(dag *ir.DAG) []api.ReferenceScopeVariable { + params := []api.ReferenceScopeVariable{} + for _, def := range dag.ParamDefs { + name := strings.TrimSpace(def.Name) + if name == "" { + continue + } + v := api.ReferenceScopeVariable{Name: name, Reference: "${params." + name + "}"} + if def.Description != "" { + v.Description = ptrOf(def.Description) + } + if def.Default != nil { + v.DefaultValue = new(fmt.Sprint(def.Default)) + } + params = append(params, v) + } + return params +} + +// scopeConsts lists the consts of dag sorted by name. Only scalar values +// (string, bool, integer and float kinds) give a default value. +func scopeConsts(dag *ir.DAG) []api.ReferenceScopeVariable { + names := make([]string, 0, len(dag.Consts)) + for name := range dag.Consts { + if strings.TrimSpace(name) != "" { + names = append(names, name) + } + } + sort.Strings(names) + + consts := make([]api.ReferenceScopeVariable, 0, len(names)) + for _, name := range names { + v := api.ReferenceScopeVariable{Name: name, Reference: "${consts." + name + "}"} + if text, ok := scalarText(dag.Consts[name]); ok { + // An empty string const keeps its empty default; ptrOf would drop it. + v.DefaultValue = &text + } + consts = append(consts, v) + } + return consts +} + +// scalarText renders value as text when it is a string, bool, integer or +// float kind. Maps, lists, nil and other kinds give false. +func scalarText(value any) (string, bool) { + if value == nil { + return "", false + } + //nolint:exhaustive // Only scalar kinds give a text. + switch reflect.ValueOf(value).Kind() { + case reflect.String, reflect.Bool, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + return fmt.Sprint(value), true + default: + return "", false + } +} + +// scopeEnv lists the names of the env entries of dag, deduplicated and sorted. +// It never reads the value of an entry: the response must not carry values, +// which can hold secrets. +func scopeEnv(dag *ir.DAG) []api.ReferenceScopeVariable { + seen := make(map[string]struct{}, len(dag.Env)) + names := make([]string, 0, len(dag.Env)) + for _, entry := range dag.Env { + name, _, found := strings.Cut(entry, "=") + name = strings.TrimSpace(name) + if !found || !envNamePattern.MatchString(name) { + continue + } + if _, dup := seen[name]; dup { + continue + } + seen[name] = struct{}{} + names = append(names, name) + } + sort.Strings(names) + + env := make([]api.ReferenceScopeVariable, 0, len(names)) + for _, name := range names { + env = append(env, api.ReferenceScopeVariable{Name: name, Reference: "${env." + name + "}"}) + } + return env +} + +// scopeSecrets lists the names of the secrets of dag, deduplicated and sorted. +// A secret reaches steps as an environment variable of its name. The scope +// never carries the value, the key, the registry ref or the options. The +// description names the provider only. +func scopeSecrets(dag *ir.DAG) []api.ReferenceScopeVariable { + byName := make(map[string]api.ReferenceScopeVariable, len(dag.Secrets)) + names := make([]string, 0, len(dag.Secrets)) + for _, secret := range dag.Secrets { + name := strings.TrimSpace(secret.Name) + if !envNamePattern.MatchString(name) { + continue + } + if _, dup := byName[name]; dup { + continue + } + v := api.ReferenceScopeVariable{Name: name, Reference: "${env." + name + "}"} + if provider := strings.TrimSpace(secret.Provider); provider != "" { + v.Description = ptrOf("Secret from provider " + provider) + } + byName[name] = v + names = append(names, name) + } + sort.Strings(names) + + secrets := make([]api.ReferenceScopeVariable, 0, len(names)) + for _, name := range names { + secrets = append(secrets, byName[name]) + } + return secrets +} diff --git a/internal/service/frontend/api/v1/editor_scope_api_test.go b/internal/service/frontend/api/v1/editor_scope_api_test.go new file mode 100644 index 0000000000..9947e72280 --- /dev/null +++ b/internal/service/frontend/api/v1/editor_scope_api_test.go @@ -0,0 +1,117 @@ +// Copyright (C) 2026 Yota Hamada +// SPDX-License-Identifier: GPL-3.0-or-later + +package api_test + +import ( + "encoding/json" + "net/http" + "testing" + + "github.com/dagucloud/dagu/v2/api/v1" + "github.com/dagucloud/dagu/v2/internal/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const scopeSecretValue = "s3cr3t-value-123" + +const scopeVariablesSpec = ` +env: + - API_TOKEN: ` + scopeSecretValue + ` + - REGION: eu +consts: + - service: api + - retries: 3 +secrets: + - name: DB_PASSWORD + provider: aws + key: prod/db/password-key-marker + options: + region: option-marker-region +steps: + - name: one + run: echo hi +` + +func validateScope(t *testing.T, name, spec string) api.ValidateDAGSpec200JSONResponse { + t.Helper() + server := test.SetupServer(t) + var body api.ValidateDAGSpec200JSONResponse + server.Client().Post("/api/v1/dags/validate", api.ValidateDAGSpecJSONRequestBody{ + Name: &name, Spec: spec, + }).ExpectStatus(http.StatusOK).Send(t).Unmarshal(t, &body) + return body +} + +func TestValidateScopeVariables(t *testing.T) { + body := validateScope(t, "scope_variables", scopeVariablesSpec) + require.True(t, body.Valid, "%v", body.Errors) + require.NotNil(t, body.ReferenceScope) + scope := body.ReferenceScope + + require.Len(t, scope.Consts, 2) + assert.Equal(t, "retries", scope.Consts[0].Name) + assert.Equal(t, "${consts.retries}", scope.Consts[0].Reference) + require.NotNil(t, scope.Consts[0].DefaultValue) + assert.Equal(t, "3", *scope.Consts[0].DefaultValue) + assert.Equal(t, "${consts.service}", scope.Consts[1].Reference) + + require.Len(t, scope.Env, 2) + assert.Equal(t, "API_TOKEN", scope.Env[0].Name) + assert.Equal(t, "${env.API_TOKEN}", scope.Env[0].Reference) + assert.Equal(t, "REGION", scope.Env[1].Name) + + require.Len(t, scope.Secrets, 1) + assert.Equal(t, "DB_PASSWORD", scope.Secrets[0].Name) + assert.Equal(t, "${env.DB_PASSWORD}", scope.Secrets[0].Reference) +} + +// The reference scope never carries an env value, a secret key or a secret +// option. The "dag" details can echo the spec, so only the scope is searched. +func TestValidateScopeHidesValues(t *testing.T) { + body := validateScope(t, "scope_hides_values", scopeVariablesSpec) + require.NotNil(t, body.ReferenceScope) + + raw, err := json.Marshal(body.ReferenceScope) + require.NoError(t, err) + for _, hidden := range []string{ + scopeSecretValue, + "password-key-marker", + "option-marker-region", + "prod/db", + } { + assert.NotContains(t, string(raw), hidden) + } + for _, v := range append(body.ReferenceScope.Env, body.ReferenceScope.Secrets...) { + assert.Nil(t, v.DefaultValue, v.Name) + } +} + +// A map const is invalid (specs/004): the load reports it and the scope lists +// no const for it. The env value must not leak from the partial scope. +func TestValidateScopeMapConst(t *testing.T) { + spec := ` +env: + - API_TOKEN: ` + scopeSecretValue + ` +consts: + - service: api + - table: + key: value +steps: + - name: one + run: echo hi +` + body := validateScope(t, "scope_map_const", spec) + require.False(t, body.Valid) + require.Len(t, body.Errors, 1) + assert.Contains(t, body.Errors[0], "consts.table") + + require.NotNil(t, body.ReferenceScope) + for _, c := range body.ReferenceScope.Consts { + assert.NotEqual(t, "table", c.Name) + } + raw, err := json.Marshal(body.ReferenceScope) + require.NoError(t, err) + assert.NotContains(t, string(raw), scopeSecretValue) +} diff --git a/internal/service/frontend/api/v1/editor_scope_steps.go b/internal/service/frontend/api/v1/editor_scope_steps.go new file mode 100644 index 0000000000..f864dd0cca --- /dev/null +++ b/internal/service/frontend/api/v1/editor_scope_steps.go @@ -0,0 +1,308 @@ +// Copyright (C) 2026 Yota Hamada +// SPDX-License-Identifier: GPL-3.0-or-later + +package api + +import ( + "cmp" + "fmt" + "maps" + "regexp" + "slices" + "strings" + + "github.com/dagucloud/dagu/v2/api/v1" + "github.com/dagucloud/dagu/v2/internal/ir" +) + +// scopeSteps lists the top-level steps of dag in DAG order, then its handler +// steps in the order init, success, failure, abort, exit, wait. +// +// Handler steps are outside the step graph (specs 007 and 009): they have no +// step-output lookup scope, so they list no upstream steps. Nothing can +// reference the outputs of a handler step either, so they list no outputs, and +// that list is static. +func scopeSteps(dag *ir.DAG) []api.ReferenceScopeStep { + steps := make([]api.ReferenceScopeStep, 0, len(dag.Steps)) + upstream := newUpstreamIndex(dag) + for i := range dag.Steps { + item := scopeStep(&dag.Steps[i], fmt.Sprintf("steps[%d]", i), upstream.names(i)) + steps = append(steps, item) + } + handlers := []struct { + event string + step *ir.Step + }{ + {"init", dag.HandlerOn.Init}, + {"success", dag.HandlerOn.Success}, + {"failure", dag.HandlerOn.Failure}, + {"abort", dag.HandlerOn.Abort}, + {"exit", dag.HandlerOn.Exit}, + {"wait", dag.HandlerOn.Wait}, + } + for _, h := range handlers { + if h.step == nil { + continue + } + item := scopeStep(h.step, "handler_on."+h.event, []string{}) + item.Outputs = []api.ReferenceScopeOutput{} + item.OutputsKnown = api.ReferenceScopeStepOutputsKnownStatic + steps = append(steps, item) + } + return steps +} + +// scopeStep describes one step with the given path and upstream names. +func scopeStep(step *ir.Step, stepPath string, upstream []string) api.ReferenceScopeStep { + item := api.ReferenceScopeStep{ + Name: step.Name, + StepPath: stepPath, + Upstream: upstream, + Outputs: []api.ReferenceScopeOutput{}, + OutputsKnown: api.ReferenceScopeStepOutputsKnownStatic, + } + if publishesRuntimeOutputs(step) { + item.OutputsKnown = api.ReferenceScopeStepOutputsKnownRuntime + } + if step.ID != "" { + item.Id = ptrOf(step.ID) + } + item.Outputs = stepOutputs(step) + return item +} + +// outputNamePattern is the name syntax of a step-output reference (spec 007). +var outputNamePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]*$`) + +// variableNamePattern is the name syntax of an output variable reference. +var variableNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) + +// outputSourceRank orders outputs by source: declared, stdoutField, +// outputVariable, action. +var outputSourceRank = map[api.ReferenceScopeOutputSource]int{ + api.ReferenceScopeOutputSourceDeclared: 0, + api.ReferenceScopeOutputSourceStdoutField: 1, + api.ReferenceScopeOutputSourceOutputVariable: 2, + api.ReferenceScopeOutputSourceAction: 3, +} + +// stepOutputs lists the outputs that other steps can reference. It orders them +// by source, then by name. A name that an earlier source already lists is not +// repeated. +// +// ${steps..outputs.} needs an id, so a step without one lists no such +// output. The output variable ${VAR} needs no id. +func stepOutputs(step *ir.Step) []api.ReferenceScopeOutput { + outputs := []api.ReferenceScopeOutput{} + seen := map[string]struct{}{} + add := func(name, typ string, source api.ReferenceScopeOutputSource) { + if step.ID == "" || !outputNamePattern.MatchString(name) { + return + } + if _, dup := seen[name]; dup { + return + } + seen[name] = struct{}{} + o := api.ReferenceScopeOutput{ + Name: name, + Reference: "${steps." + step.ID + ".outputs." + name + "}", + Source: source, + } + if typ != "" { + o.Type = ptrOf(typ) + } + outputs = append(outputs, o) + } + + for _, out := range step.Outputs { + if out.Source != ir.StepDeclaredOutputSourceCapture { + add(out.Name, out.Type, api.ReferenceScopeOutputSourceDeclared) + } + } + if cfg := step.StdoutOutputs; cfg != nil { + names := slices.Sorted(maps.Keys(cfg.Fields)) + if len(names) == 0 && cfg.Field != "" { + names = []string{cfg.Field} + } + for _, name := range names { + add(name, "", api.ReferenceScopeOutputSourceStdoutField) + } + } + + // The build derives the names that an action publishes (a custom action + // output_schema, outputs.write, browser extracts) into Outputs with the + // capture source. A step that runs a plain command publishes its derived + // names through its own output or output_schema field: they are declared. + captured := api.ReferenceScopeOutputSourceDeclared + if !step.ExecutorConfig.IsCommand() || isCustomAction(step) { + captured = api.ReferenceScopeOutputSourceAction + } + for _, out := range step.Outputs { + if out.Source == ir.StepDeclaredOutputSourceCapture { + add(out.Name, out.Type, captured) + } + } + + if name := strings.TrimPrefix(step.Output, "$"); variableNamePattern.MatchString(name) { + outputs = append(outputs, api.ReferenceScopeOutput{ + Name: name, + Reference: "${" + name + "}", + Source: api.ReferenceScopeOutputSourceOutputVariable, + }) + } + + slices.SortStableFunc(outputs, func(a, b api.ReferenceScopeOutput) int { + if c := cmp.Compare(outputSourceRank[a.Source], outputSourceRank[b.Source]); c != 0 { + return c + } + return strings.Compare(a.Name, b.Name) + }) + return outputs +} + +// publishesRuntimeOutputs reports whether only a run reveals the output names of +// the step. It follows the rule of the value resolver (publishesRuntimeOnlyOutputs +// in internal/spec/value_reference_notices_report.go): a sub-DAG step takes its +// names from the child document, and a bundle action from a manifest. A custom +// action is also runtime, because its template can publish more than its +// output_schema lists. The remaining cases are steps whose captured names are +// dynamic. Browser, computer and xlsx extract steps are not checked. +func publishesRuntimeOutputs(step *ir.Step) bool { + switch step.ExecutorConfig.Type { + case ir.ExecutorTypeDAG, ir.ExecutorTypeSubworkflow, ir.ExecutorTypeAction: + return true + } + if step.SubDAG != nil || isCustomAction(step) { + return true + } + switch { + case step.HasStdoutOutputs(): + // Decoding the whole stdout into outputs reveals the keys only at run time. + return len(step.StdoutOutputs.Fields) == 0 && step.StdoutOutputs.Field == "" + case step.HasStructuredOutput(): + return false + case step.HasOutputSchema(): + if _, ok := step.OutputSchema["properties"].(map[string]any); !ok { + return true + } + // An open schema accepts names that it does not list. + return step.ExecutorConfig.Type != ir.ExecutorTypeChat && !schemaForbidsExtraProperties(step.OutputSchema) + case step.ExecutorConfig.Type == ir.ExecutorTypeOutputs: + _, ok := step.ExecutorConfig.Config["values"].(map[string]any) + return !ok + } + return false +} + +// schemaForbidsExtraProperties reports whether a schema accepts only the names +// it lists. A pattern-matched property counts as an unlisted name. +func schemaForbidsExtraProperties(schema map[string]any) bool { + if _, ok := schema["patternProperties"]; ok { + return false + } + additional, ok := schema["additionalProperties"].(bool) + return ok && !additional +} + +// isCustomAction reports whether the build expanded the step from a custom +// action or a custom step type. Its executor is the one of the template. +func isCustomAction(step *ir.Step) bool { + name, _ := step.ExecutorConfig.Metadata["custom_type"].(string) + return name != "" +} + +// upstreamIndex holds the ancestor sets of the top-level steps of one DAG. +type upstreamIndex struct { + steps []ir.Step + // ancestors[i] is a bit set over step indexes, filled once by a memoized DFS. + ancestors [][]uint64 +} + +// newUpstreamIndex computes the transitive ancestors of every top-level step. +// A dependency on an unknown step name is ignored. +func newUpstreamIndex(dag *ir.DAG) *upstreamIndex { + n := len(dag.Steps) + idx := &upstreamIndex{steps: dag.Steps, ancestors: make([][]uint64, n)} + byName := make(map[string]int, n) + for i := range dag.Steps { + byName[dag.Steps[i].Name] = i + } + parents := make([][]int, n) + for i := range dag.Steps { + for _, dep := range dag.Steps[i].AllDepends() { + if j, ok := byName[dep]; ok { + parents[i] = append(parents[i], j) + } + } + } + + const ( + unvisited = iota + inProgress + done + ) + state := make([]int, n) + words := (n + 63) / 64 + cyclic := false + var visit func(i int) + visit = func(i int) { + state[i] = inProgress + set := make([]uint64, words) + for _, p := range parents[i] { + switch state[p] { + case inProgress: + cyclic = true + continue + case unvisited: + visit(p) + } + set[p/64] |= 1 << (p % 64) + for w, bits := range idx.ancestors[p] { + set[w] |= bits + } + } + idx.ancestors[i] = set + state[i] = done + } + for i := range n { + if state[i] == unvisited { + visit(i) + } + } + if cyclic { + // A memoized set is incomplete for steps on a cycle: recompute them by search. + for i := range n { + idx.ancestors[i] = reachableFrom(i, parents, words) + } + } + return idx +} + +// reachableFrom returns the steps reachable from start through parents. +// Start itself is included only when it lies on a cycle. +func reachableFrom(start int, parents [][]int, words int) []uint64 { + set := make([]uint64, words) + stack := append([]int(nil), parents[start]...) + for len(stack) > 0 { + p := stack[len(stack)-1] + stack = stack[:len(stack)-1] + if set[p/64]&(1<<(p%64)) != 0 { + continue + } + set[p/64] |= 1 << (p % 64) + stack = append(stack, parents[p]...) + } + return set +} + +// names lists the ancestors of step i in DAG order, never step i itself. +// The slice is never nil. +func (u *upstreamIndex) names(i int) []string { + out := []string{} + for j := range u.steps { + if j != i && u.ancestors[i][j/64]&(1<<(j%64)) != 0 { + out = append(out, u.steps[j].Name) + } + } + return out +} diff --git a/internal/service/frontend/api/v1/editor_scope_steps_test.go b/internal/service/frontend/api/v1/editor_scope_steps_test.go new file mode 100644 index 0000000000..ee3e01e450 --- /dev/null +++ b/internal/service/frontend/api/v1/editor_scope_steps_test.go @@ -0,0 +1,587 @@ +// Copyright (C) 2026 Yota Hamada +// SPDX-License-Identifier: GPL-3.0-or-later + +package api + +import ( + "context" + "fmt" + "strings" + "testing" + "time" + + "github.com/dagucloud/dagu/v2/api/v1" + cmnvalue "github.com/dagucloud/dagu/v2/internal/cmn/value" + "github.com/dagucloud/dagu/v2/internal/ir" + "github.com/dagucloud/dagu/v2/internal/spec" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// loadScopeDAG loads YAML the way the editor does: build errors are kept and +// nothing is evaluated. +func loadScopeDAG(t *testing.T, yaml string) *ir.DAG { + t.Helper() + dag, err := spec.LoadYAML(context.Background(), []byte(yaml), spec.WithAllowBuildErrors(), spec.WithoutEval()) + require.NoError(t, err) + require.NotNil(t, dag) + return dag +} + +// scopeStepsByName indexes the scope steps of dag by name. +func scopeStepsByName(t *testing.T, dag *ir.DAG) map[string]api.ReferenceScopeStep { + t.Helper() + out := map[string]api.ReferenceScopeStep{} + for _, s := range scopeSteps(dag) { + out[s.Name] = s + } + return out +} + +func TestScopeUpstreamTransitive(t *testing.T) { + dag := loadScopeDAG(t, ` +type: graph +steps: + - name: a + run: echo a + - name: b + run: echo b + depends: a + - name: c + run: echo c + depends: b + - name: d + run: echo d + - name: e + run: echo e + depends: [c, d] +`) + got := scopeStepsByName(t, dag) + assert.Equal(t, []string{}, got["a"].Upstream) + assert.Equal(t, []string{"a"}, got["b"].Upstream) + assert.Equal(t, []string{"a", "b"}, got["c"].Upstream) + assert.Equal(t, []string{}, got["d"].Upstream) + // Ancestors come in DAG order, not in dependency order. + assert.Equal(t, []string{"a", "b", "c", "d"}, got["e"].Upstream) +} + +// A cycle must give a finite answer: every step of the cycle lists the others. +func TestScopeUpstreamCycle(t *testing.T) { + dag := &ir.DAG{Steps: []ir.Step{ + {Name: "a", Depends: []string{"c"}}, + {Name: "b", Depends: []string{"a"}}, + {Name: "c", Depends: []string{"b"}}, + {Name: "d", Depends: []string{"d", "missing"}}, + }} + got := scopeStepsByName(t, dag) + assert.Equal(t, []string{"b", "c"}, got["a"].Upstream) + assert.Equal(t, []string{"a", "c"}, got["b"].Upstream) + assert.Equal(t, []string{"a", "b"}, got["c"].Upstream) + assert.Equal(t, []string{}, got["d"].Upstream) +} + +// An output reference orders its step after the producer without depends. +func TestScopeUpstreamInferred(t *testing.T) { + dag := loadScopeDAG(t, ` +type: graph +steps: + - id: build + run: echo build + outputs: + - name: tag + - id: mid + run: echo ${steps.build.outputs.tag} + - id: deploy + run: echo ${steps.mid.outputs.x} + depends: [] +`) + got := scopeStepsByName(t, dag) + assert.Equal(t, []string{}, got["build"].Upstream) + assert.Equal(t, []string{"build"}, got["mid"].Upstream) + assert.Equal(t, []string{"build", "mid"}, got["deploy"].Upstream) +} + +// The builder writes the router into the depends of every target, so the +// closure needs no router-specific rule. +func TestScopeUpstreamRouter(t *testing.T) { + dag := loadScopeDAG(t, ` +type: graph +steps: + - name: fetch + run: echo fetch + - name: route + type: router + depends: fetch + value: ${MODE} + routes: + a: [step_a] + b: [step_b, step_c] + - name: step_a + run: echo a + - name: step_b + run: echo b + - name: step_c + run: echo c + depends: step_b + - name: join + run: echo join + depends: [step_a, step_c] +`) + got := scopeStepsByName(t, dag) + assert.Equal(t, []string{"fetch"}, got["route"].Upstream) + assert.Equal(t, []string{"fetch", "route"}, got["step_a"].Upstream) + assert.Equal(t, []string{"fetch", "route"}, got["step_b"].Upstream) + assert.Equal(t, []string{"fetch", "route", "step_b"}, got["step_c"].Upstream) + assert.Equal(t, []string{"fetch", "route", "step_a", "step_b", "step_c"}, got["join"].Upstream) +} + +// A chain DAG has no depends in the YAML: the builder links each step to the +// previous one, and an id does not change the resulting names. +func TestScopeUpstreamChain(t *testing.T) { + dag := loadScopeDAG(t, ` +type: chain +steps: + - name: one + id: first + run: echo 1 + - name: two + run: echo 2 + - name: three + run: echo 3 +`) + got := scopeStepsByName(t, dag) + assert.Equal(t, []string{}, got["one"].Upstream) + assert.Equal(t, []string{"one"}, got["two"].Upstream) + assert.Equal(t, []string{"one", "two"}, got["three"].Upstream) +} + +// A nested array of a chain DAG is a group of steps that share the previous +// entry; the next entry depends on the whole group. +func TestScopeUpstreamChainGroup(t *testing.T) { + dag := loadScopeDAG(t, ` +type: chain +steps: + - name: first + run: echo 1 + - + - name: left + run: echo l + - name: right + run: echo r + - name: last + run: echo z +`) + got := scopeStepsByName(t, dag) + assert.Equal(t, []string{"first"}, got["left"].Upstream) + assert.Equal(t, []string{"first"}, got["right"].Upstream) + assert.Equal(t, []string{"first", "left", "right"}, got["last"].Upstream) +} + +// Handler steps follow the top-level steps in a fixed event order and have no +// upstream steps or referenceable outputs. +func TestScopeHandlers(t *testing.T) { + dag := loadScopeDAG(t, ` +type: graph +handler_on: + wait: + run: echo wait + exit: + run: echo exit + failure: + run: echo failure + abort: + run: echo abort + success: + run: echo success + init: + run: echo init +steps: + - id: main + run: echo main + outputs: + - name: x +`) + steps := scopeSteps(dag) + require.Len(t, steps, 7) + assert.Equal(t, "steps[0]", steps[0].StepPath) + wantPaths := []string{ + "handler_on.init", "handler_on.success", "handler_on.failure", + "handler_on.abort", "handler_on.exit", "handler_on.wait", + } + for i, path := range wantPaths { + h := steps[i+1] + assert.Equal(t, path, h.StepPath) + assert.NotEmpty(t, h.Name) + assert.Equal(t, []string{}, h.Upstream) + assert.Equal(t, []api.ReferenceScopeOutput{}, h.Outputs) + assert.Equal(t, api.ReferenceScopeStepOutputsKnownStatic, h.OutputsKnown) + } +} + +// A handler step with an id still lists nothing: no field can resolve it, so +// the empty list is final even when only a run would give its names. +func TestScopeHandlerWithID(t *testing.T) { + dag := loadScopeDAG(t, ` +type: graph +handler_on: + exit: + id: cleanup + run: echo done + outputs: + - name: result + failure: + id: report + run: echo done + stdout: + outputs: + decode: json +steps: + - run: echo main + name: main +`) + steps := scopeSteps(dag) + require.Len(t, steps, 3) + assert.Equal(t, "handler_on.failure", steps[1].StepPath) + assert.Equal(t, "handler_on.exit", steps[2].StepPath) + require.NotNil(t, steps[2].Id) + assert.Equal(t, "cleanup", *steps[2].Id) + for _, h := range steps[1:] { + assert.Empty(t, h.Outputs, h.StepPath) + assert.Equal(t, api.ReferenceScopeStepOutputsKnownStatic, h.OutputsKnown, h.StepPath) + } +} + +func outputRefs(outputs []api.ReferenceScopeOutput) []string { + refs := make([]string, 0, len(outputs)) + for _, o := range outputs { + refs = append(refs, string(o.Source)+" "+o.Reference) + } + return refs +} + +func TestScopeOutputsStdoutFields(t *testing.T) { + dag := loadScopeDAG(t, ` +type: graph +steps: + - id: build + run: echo ok + outputs: + - name: tag + type: string + stdout: + outputs: + fields: + zeta: {decode: json, select: .z} + image: {decode: json, select: .image} + tag: {decode: json, select: .tag} + - id: single + run: echo ok + stdout: + outputs: + field: result + decode: json + - id: whole + run: echo ok + stdout: + outputs: + decode: json + - run: echo ok + name: noid + stdout: + outputs: + fields: + x: {decode: json} +`) + got := scopeStepsByName(t, dag) + // A declared name wins over a stdout field of the same name. + assert.Equal(t, []string{ + "declared ${steps.build.outputs.tag}", + "stdoutField ${steps.build.outputs.image}", + "stdoutField ${steps.build.outputs.zeta}", + }, outputRefs(got["build"].Outputs)) + require.NotNil(t, got["build"].Outputs[0].Type) + assert.Equal(t, "string", *got["build"].Outputs[0].Type) + assert.Nil(t, got["build"].Outputs[1].Type) + assert.Equal(t, []string{"stdoutField ${steps.single.outputs.result}"}, outputRefs(got["single"].Outputs)) + assert.Empty(t, got["whole"].Outputs) + assert.Empty(t, got["noid"].Outputs) +} + +// The output variable ${VAR} needs no id, so it is listed for every step. +func TestScopeOutputsVariable(t *testing.T) { + dag := loadScopeDAG(t, ` +type: graph +steps: + - id: fetch + run: echo ok + output: BODY + outputs: + - name: size + - run: echo ok + name: noid + output: $RESULT + - run: echo ok + name: plain + - run: echo ok + name: object + output: + image: + from: stdout +`) + got := scopeStepsByName(t, dag) + assert.Equal(t, []string{ + "declared ${steps.fetch.outputs.size}", + "outputVariable ${BODY}", + }, outputRefs(got["fetch"].Outputs)) + assert.Equal(t, []string{"outputVariable ${RESULT}"}, outputRefs(got["noid"].Outputs)) + assert.Equal(t, "RESULT", got["noid"].Outputs[0].Name) + assert.Empty(t, got["plain"].Outputs) + // Object-form output publishes step outputs, not a variable. + assert.Empty(t, got["object"].Outputs) +} + +func TestScopeOutputsCustomAction(t *testing.T) { + dag := loadScopeDAG(t, ` +type: graph +actions: + build.image: + input_schema: {type: object, additionalProperties: false, properties: {}} + output_schema: + type: object + additionalProperties: false + properties: + tag: {type: string} + meta: {type: object} + template: + run: echo ok +steps: + - id: build + action: build.image + with: {} + - id: write + action: outputs.write + with: + values: + answer: "42" + - id: plain + run: echo ok + output_schema: + type: object + properties: + found: {type: string} +`) + got := scopeStepsByName(t, dag) + build := got["build"].Outputs + assert.Equal(t, []string{ + "action ${steps.build.outputs.meta}", + "action ${steps.build.outputs.tag}", + }, outputRefs(build)) + require.NotNil(t, build[0].Type) + assert.Equal(t, "json", *build[0].Type) + require.NotNil(t, build[1].Type) + assert.Equal(t, "string", *build[1].Type) + assert.Equal(t, []string{"action ${steps.write.outputs.answer}"}, outputRefs(got["write"].Outputs)) + // A command step publishes its own output_schema names. + assert.Equal(t, []string{"declared ${steps.plain.outputs.found}"}, outputRefs(got["plain"].Outputs)) +} + +// Steps whose output names only a run reveals are marked runtime. A static +// step has a closed list of names. +func TestScopeOutputsKnown(t *testing.T) { + dag := loadScopeDAG(t, ` +type: graph +actions: + build.image: + input_schema: {type: object, additionalProperties: false, properties: {}} + output_schema: + type: object + properties: + tag: {type: string} + template: + run: echo ok +steps: + - id: plain + run: echo ok + outputs: + - name: x + - id: closed + run: echo ok + output_schema: + type: object + additionalProperties: false + properties: + found: {type: string} + - id: open + run: echo ok + output_schema: + type: object + properties: + found: {type: string} + - id: whole + run: echo ok + stdout: + outputs: + decode: json + - id: fields + run: echo ok + stdout: + outputs: + fields: + a: {decode: json} + - id: child + call: child + - id: custom + action: build.image + with: {} + - id: bundle + action: acme/dagu-actions-slack@v1 +`) + want := map[string]api.ReferenceScopeStepOutputsKnown{ + "plain": api.ReferenceScopeStepOutputsKnownStatic, + "closed": api.ReferenceScopeStepOutputsKnownStatic, + "open": api.ReferenceScopeStepOutputsKnownRuntime, + "whole": api.ReferenceScopeStepOutputsKnownRuntime, + "fields": api.ReferenceScopeStepOutputsKnownStatic, + "child": api.ReferenceScopeStepOutputsKnownRuntime, + "custom": api.ReferenceScopeStepOutputsKnownRuntime, + "bundle": api.ReferenceScopeStepOutputsKnownRuntime, + } + got := map[string]api.ReferenceScopeStepOutputsKnown{} + for _, s := range scopeSteps(dag) { + got[s.Name] = s.OutputsKnown + } + assert.Equal(t, want, got) +} + +// Every ${steps...} reference that the scope returns must pass the value +// resolver without a notice. A reference to a missing output still gets one, +// which shows that the check can fail. +func TestScopeReferencesResolve(t *testing.T) { + const producers = ` +type: graph +actions: + build.image: + input_schema: {type: object, additionalProperties: false, properties: {}} + output_schema: + type: object + properties: + tag: {type: string} + meta: {type: object} + template: + run: echo ok +steps: + - id: declared + run: echo ok + output: BODY + outputs: + - name: size + - name: blob + type: json + - id: fields + run: echo ok + stdout: + outputs: + fields: + image: {decode: json, select: .image} + zeta: {decode: json} + - id: single + run: echo ok + stdout: + outputs: + field: result + decode: json + - id: schema + run: echo ok + output_schema: + type: object + additionalProperties: false + properties: + found: {type: string} + - id: write + action: outputs.write + with: + values: + answer: "42" + - id: custom + action: build.image + with: {} + - run: echo ok + name: noid + output: RESULT +` + scope := buildReferenceScope(loadScopeDAG(t, producers)) + require.NotNil(t, scope) + var refs []string + for _, step := range scope.Steps { + for _, o := range step.Outputs { + refs = append(refs, o.Reference) + } + } + require.Len(t, refs, 11) + assert.Contains(t, refs, "${RESULT}") + + var consumer strings.Builder + consumer.WriteString(" - id: consumer\n run: echo done\n env:\n") + for i, ref := range refs { + fmt.Fprintf(&consumer, " - V%d: %q\n", i, ref) + } + consumer.WriteString(" - MISSING: \"${steps.declared.outputs.nosuch}\"\n") + + result, err := spec.LoadYAMLWithResult(context.Background(), []byte(producers+consumer.String()), + spec.WithAllowBuildErrors(), spec.WithoutEval()) + require.NoError(t, err) + + // An output variable is not known before a run and no dependency is + // inferred from it, so the resolver reports it as a runtime-only value. + got := map[string]cmnvalue.ValueReferenceNotice{} + for _, n := range result.ValueReferenceNotices { + got[n.Token] = n + } + require.Len(t, got, 3) + for _, token := range []string{"${BODY}", "${RESULT}"} { + assert.Equal(t, cmnvalue.ValueReferenceReasonUnknownEnvBinding, got[token].Reason, token) + assert.Equal(t, cmnvalue.NoticeClassRuntimeOnly, got[token].Class, token) + } + assert.Equal(t, cmnvalue.ValueReferenceReasonUnknownOutputName, got["${steps.declared.outputs.nosuch}"].Reason) +} + +// The closure is computed once per DAG, so 300 steps with a long chain and a +// wide fan-in stay far below the generous bound of the test. +func TestScopeUpstreamLarge(t *testing.T) { + const n = 300 + tests := []struct { + name string + depends func(i int) string + }{ + {"chain", func(i int) string { return fmt.Sprintf("[s%d]", i-1) }}, + {"fan-in", func(i int) string { return fmt.Sprintf("[s%d, s%d]", i-1, i/2) }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var yaml strings.Builder + yaml.WriteString("type: graph\nsteps:\n") + for i := range n { + fmt.Fprintf(&yaml, " - name: s%d\n run: echo %d\n", i, i) + if i > 0 { + fmt.Fprintf(&yaml, " depends: %s\n", tt.depends(i)) + } + } + dag := loadScopeDAG(t, yaml.String()) + require.Len(t, dag.Steps, n) + + start := time.Now() + steps := scopeSteps(dag) + elapsed := time.Since(start) + + require.Len(t, steps, n) + for i, step := range steps { + require.Len(t, step.Upstream, i, step.Name) + if i > 0 { + assert.Equal(t, "s0", step.Upstream[0]) + assert.Equal(t, fmt.Sprintf("s%d", i-1), step.Upstream[i-1]) + } + } + assert.Less(t, elapsed, 2*time.Second) + }) + } +} diff --git a/internal/service/frontend/api/v1/editor_scope_test.go b/internal/service/frontend/api/v1/editor_scope_test.go new file mode 100644 index 0000000000..e509db8b55 --- /dev/null +++ b/internal/service/frontend/api/v1/editor_scope_test.go @@ -0,0 +1,336 @@ +// Copyright (C) 2026 Yota Hamada +// SPDX-License-Identifier: GPL-3.0-or-later + +package api + +import ( + "context" + "encoding/json" + "testing" + + "github.com/dagucloud/dagu/v2/api/v1" + "github.com/dagucloud/dagu/v2/internal/ir" + secretref "github.com/dagucloud/dagu/v2/internal/secret/ref" + "github.com/dagucloud/dagu/v2/internal/spec" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBuildReferenceScopeNil(t *testing.T) { + assert.Nil(t, buildReferenceScope(nil)) +} + +func TestBuildReferenceScopeEmpty(t *testing.T) { + scope := buildReferenceScope(&ir.DAG{}) + require.NotNil(t, scope) + assert.NotNil(t, scope.Params) + assert.NotNil(t, scope.Consts) + assert.NotNil(t, scope.Env) + assert.NotNil(t, scope.Secrets) + assert.NotNil(t, scope.Steps) +} + +func TestBuildReferenceScope(t *testing.T) { + dag := &ir.DAG{ + ParamDefs: []ir.ParamDef{ + {Name: "region", Default: "eu", Description: "target"}, + {Name: "count", Default: 3}, + {Name: "free"}, + {Name: "blank", Default: ""}, + {}, + }, + Steps: []ir.Step{ + { + Name: "fetch", + ID: "fetch", + Outputs: []ir.StepOutputDeclaration{ + {Name: "body", Type: "json"}, + }, + }, + {Name: "plain", Depends: []string{"fetch"}, Outputs: []ir.StepOutputDeclaration{{Name: "x"}}}, + }, + } + scope := buildReferenceScope(dag) + require.NotNil(t, scope) + + require.Len(t, scope.Params, 4) + assert.Equal(t, "${params.region}", scope.Params[0].Reference) + require.NotNil(t, scope.Params[0].DefaultValue) + assert.Equal(t, "eu", *scope.Params[0].DefaultValue) + require.NotNil(t, scope.Params[0].Description) + assert.Equal(t, "3", *scope.Params[1].DefaultValue) + assert.Nil(t, scope.Params[2].DefaultValue) + // An empty default is a default, unlike no default. + require.NotNil(t, scope.Params[3].DefaultValue) + assert.Empty(t, *scope.Params[3].DefaultValue) + + require.Len(t, scope.Steps, 2) + fetch := scope.Steps[0] + assert.Equal(t, "steps[0]", fetch.StepPath) + assert.Equal(t, []string{}, fetch.Upstream) + assert.Equal(t, api.ReferenceScopeStepOutputsKnownStatic, fetch.OutputsKnown) + require.Len(t, fetch.Outputs, 1) + assert.Equal(t, "${steps.fetch.outputs.body}", fetch.Outputs[0].Reference) + assert.Equal(t, api.ReferenceScopeOutputSourceDeclared, fetch.Outputs[0].Source) + require.NotNil(t, fetch.Outputs[0].Type) + assert.Equal(t, "json", *fetch.Outputs[0].Type) + + plain := scope.Steps[1] + assert.Equal(t, "steps[1]", plain.StepPath) + assert.Nil(t, plain.Id) + assert.Equal(t, []string{"fetch"}, plain.Upstream) + // A step without an id cannot be referenced. + assert.Empty(t, plain.Outputs) + assert.NotNil(t, plain.Outputs) +} + +func TestScopeConsts(t *testing.T) { + ptr := func(s string) *string { return &s } + tests := []struct { + name string + consts map[string]any + want []api.ReferenceScopeVariable + }{ + {name: "none", consts: nil, want: []api.ReferenceScopeVariable{}}, + { + name: "sorted scalars", + consts: map[string]any{ + "zeta": "last", + "alpha": true, + "count": int64(4), + "ratio": 1.5, + "size": uint8(7), + "version": json.Number("12"), + }, + want: []api.ReferenceScopeVariable{ + {Name: "alpha", Reference: "${consts.alpha}", DefaultValue: ptr("true")}, + {Name: "count", Reference: "${consts.count}", DefaultValue: ptr("4")}, + {Name: "ratio", Reference: "${consts.ratio}", DefaultValue: ptr("1.5")}, + {Name: "size", Reference: "${consts.size}", DefaultValue: ptr("7")}, + {Name: "version", Reference: "${consts.version}", DefaultValue: ptr("12")}, + {Name: "zeta", Reference: "${consts.zeta}", DefaultValue: ptr("last")}, + }, + }, + { + name: "empty string is a scalar", + consts: map[string]any{"blank": ""}, + want: []api.ReferenceScopeVariable{ + {Name: "blank", Reference: "${consts.blank}", DefaultValue: ptr("")}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, scopeConsts(&ir.DAG{Consts: tt.consts})) + }) + } +} + +// Maps, lists and nil keep their name but give no default value. +func TestScopeConstsNonScalar(t *testing.T) { + consts := scopeConsts(&ir.DAG{Consts: map[string]any{ + "list": []any{"a", "b"}, + "map": map[string]any{"k": "v"}, + "nil": nil, + }}) + require.Len(t, consts, 3) + for _, c := range consts { + assert.Nil(t, c.DefaultValue, c.Name) + assert.Equal(t, "${consts."+c.Name+"}", c.Reference) + } +} + +// Consts survive a load without evaluation and keep their load-time value. +func TestScopeConstsFromYAML(t *testing.T) { + const doc = ` +consts: + - service: api + - endpoint: http://localhost/${consts.service} + - enabled: true + - retries: 3 +steps: + - name: one + run: echo ${consts.endpoint} +` + dag, err := spec.LoadYAML(context.Background(), []byte(doc), spec.WithAllowBuildErrors(), spec.WithoutEval()) + require.NoError(t, err) + + scope := buildReferenceScope(dag) + require.NotNil(t, scope) + got := map[string]string{} + names := []string{} + for _, c := range scope.Consts { + names = append(names, c.Name) + require.NotNil(t, c.DefaultValue, c.Name) + got[c.Name] = *c.DefaultValue + } + assert.Equal(t, []string{"enabled", "endpoint", "retries", "service"}, names) + assert.Equal(t, "http://localhost/api", got["endpoint"]) + assert.Equal(t, "true", got["enabled"]) + assert.Equal(t, "3", got["retries"]) +} + +func TestScopeEnv(t *testing.T) { + tests := []struct { + name string + env []string + want []string + }{ + {name: "none", env: nil, want: []string{}}, + { + name: "sorted and deduplicated", + env: []string{"ZONE=eu", "API_URL=http://x", "ZONE=us", "_PRIVATE=1"}, + want: []string{"API_URL", "ZONE", "_PRIVATE"}, + }, + { + name: "empty value and equal signs in value", + env: []string{"EMPTY=", "DSN=host=db user=app"}, + want: []string{"DSN", "EMPTY"}, + }, + { + name: "invalid entries are skipped", + env: []string{"NOEQUALS", "=value", "1BAD=x", "has space=x", "OK=1"}, + want: []string{"OK"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := scopeEnv(&ir.DAG{Env: tt.env}) + require.NotNil(t, got) + names := make([]string, 0, len(got)) + for _, v := range got { + names = append(names, v.Name) + assert.Equal(t, "${env."+v.Name+"}", v.Reference) + } + assert.Equal(t, tt.want, names) + }) + } +} + +// An env value never reaches the scope, in any field. +func TestScopeEnvHidesValues(t *testing.T) { + const secret = "s3cr3t-value-123" + got := scopeEnv(&ir.DAG{Env: []string{"API_TOKEN=" + secret, "OTHER=" + secret}}) + require.Len(t, got, 2) + raw, err := json.Marshal(got) + require.NoError(t, err) + assert.NotContains(t, string(raw), secret) + for _, v := range got { + assert.Nil(t, v.DefaultValue) + assert.Nil(t, v.Description) + } +} + +func TestScopeEnvFromYAML(t *testing.T) { + const doc = ` +env: + - API_URL: http://x + - API_TOKEN: s3cr3t-value-123 + - REGION: ${env.API_URL} +steps: + - name: one + run: echo hi +` + dag, err := spec.LoadYAML(context.Background(), []byte(doc), spec.WithAllowBuildErrors(), spec.WithoutEval()) + require.NoError(t, err) + + scope := buildReferenceScope(dag) + require.NotNil(t, scope) + names := []string{} + for _, v := range scope.Env { + names = append(names, v.Name) + } + assert.Equal(t, []string{"API_TOKEN", "API_URL", "REGION"}, names) +} + +func TestScopeSecrets(t *testing.T) { + desc := func(s string) *string { return &s } + tests := []struct { + name string + secrets []secretref.Ref + want []api.ReferenceScopeVariable + }{ + {name: "none", want: []api.ReferenceScopeVariable{}}, + { + name: "sorted and deduplicated", + secrets: []secretref.Ref{ + {Name: "DB_PASSWORD", Provider: "aws", Key: "prod/db/password"}, + {Name: "API_TOKEN", Provider: "env", Key: "SOURCE_API_TOKEN"}, + {Name: "DB_PASSWORD", Provider: "file", Key: "/run/db"}, + }, + want: []api.ReferenceScopeVariable{ + {Name: "API_TOKEN", Reference: "${env.API_TOKEN}", Description: desc("Secret from provider env")}, + {Name: "DB_PASSWORD", Reference: "${env.DB_PASSWORD}", Description: desc("Secret from provider aws")}, + }, + }, + { + name: "registry ref has no description", + secrets: []secretref.Ref{{Name: "REG_SECRET", Ref: "team/registry-secret"}}, + want: []api.ReferenceScopeVariable{ + {Name: "REG_SECRET", Reference: "${env.REG_SECRET}"}, + }, + }, + { + name: "invalid names are skipped", + secrets: []secretref.Ref{{Name: ""}, {Name: "bad name", Provider: "env", Key: "X"}}, + want: []api.ReferenceScopeVariable{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, scopeSecrets(&ir.DAG{Secrets: tt.secrets})) + }) + } +} + +// Value, key, ref and options never reach the scope. +func TestScopeSecretsHideValues(t *testing.T) { + got := scopeSecrets(&ir.DAG{Secrets: []secretref.Ref{ + { + Name: "API_TOKEN", + Provider: "aws", + Key: "key-marker-77", + Options: map[string]string{"region": "option-marker-88", "field": "field-marker-99"}, + }, + {Name: "REG", Ref: "ref-marker-66"}, + }}) + raw, err := json.Marshal(got) + require.NoError(t, err) + for _, marker := range []string{"key-marker-77", "option-marker-88", "field-marker-99", "ref-marker-66"} { + assert.NotContains(t, string(raw), marker) + } + for _, v := range got { + assert.Nil(t, v.DefaultValue) + } +} + +func TestScopeSecretsFromYAML(t *testing.T) { + const doc = ` +secrets: + - name: DB_PASSWORD + provider: aws + key: prod/db/password + options: + region: us-east-1 + - name: API_TOKEN + provider: env + key: SOURCE_API_TOKEN +steps: + - name: one + run: echo hi +` + dag, err := spec.LoadYAML(context.Background(), []byte(doc), spec.WithAllowBuildErrors(), spec.WithoutEval()) + require.NoError(t, err) + + scope := buildReferenceScope(dag) + require.NotNil(t, scope) + names := []string{} + for _, v := range scope.Secrets { + names = append(names, v.Name) + } + assert.Equal(t, []string{"API_TOKEN", "DB_PASSWORD"}, names) + raw, err := json.Marshal(scope) + require.NoError(t, err) + assert.NotContains(t, string(raw), "SOURCE_API_TOKEN") + assert.NotContains(t, string(raw), "prod/db/password") +} diff --git a/internal/service/frontend/api/v1/editor_step_outputs.go b/internal/service/frontend/api/v1/editor_step_outputs.go new file mode 100644 index 0000000000..4f8181195b --- /dev/null +++ b/internal/service/frontend/api/v1/editor_step_outputs.go @@ -0,0 +1,163 @@ +// Copyright (C) 2026 Yota Hamada +// SPDX-License-Identifier: GPL-3.0-or-later + +package api + +import ( + "context" + "fmt" + "sort" + "strings" + "unicode/utf8" + + "github.com/dagucloud/dagu/v2/api/v1" + "github.com/dagucloud/dagu/v2/internal/ir" + runtimepkg "github.com/dagucloud/dagu/v2/internal/runtime" +) + +// maxStepOutputBytes is the size at which a step output value is cut. +const maxStepOutputBytes = 65536 + +// GetDAGRunStepOutputs returns the persisted outputs of one step of a DAG-run. +// The runtime masks secret values before it persists them; this handler never +// changes them. +func (a *API) GetDAGRunStepOutputs(ctx context.Context, request api.GetDAGRunStepOutputsRequestObject) (api.GetDAGRunStepOutputsResponseObject, error) { + attempt, notFound, err := a.findVisibleAttempt(ctx, request.Name, request.DagRunId) + if err != nil { + return nil, err + } + if notFound != "" { + return api.GetDAGRunStepOutputs404JSONResponse{Code: api.ErrorCodeNotFound, Message: notFound}, nil + } + + status, err := attempt.ReadStatus(ctx) + if err != nil { + return nil, fmt.Errorf("error reading dag-run status: %w", err) + } + var node *ir.Node + if status != nil { + node = findStepOutputsNode(status, request.StepName) + } + if node == nil { + return api.GetDAGRunStepOutputs404JSONResponse{ + Code: api.ErrorCodeNotFound, + Message: fmt.Sprintf("step %s not found in dag-run of DAG %s", request.StepName, request.Name), + }, nil + } + + return api.GetDAGRunStepOutputs200JSONResponse(toStepOutputs(node)), nil +} + +// findStepOutputsNode resolves ref in this order: top-level step name, +// top-level step id, handler step name, handler step id, handler key. +func findStepOutputsNode(status *ir.DAGRunStatus, ref string) *ir.Node { + for _, node := range status.Nodes { + if node != nil && node.Step.Name == ref { + return node + } + } + for _, node := range status.Nodes { + if node != nil && node.Step.ID != "" && node.Step.ID == ref { + return node + } + } + handlers := []struct { + key string + node *ir.Node + }{ + {"handler_on.init", status.OnInit}, + {"handler_on.success", status.OnSuccess}, + {"handler_on.failure", status.OnFailure}, + {"handler_on.abort", status.OnAbort}, + {"handler_on.exit", status.OnExit}, + {"handler_on.wait", status.OnWait}, + } + for _, h := range handlers { + if h.node != nil && h.node.Step.Name == ref { + return h.node + } + } + for _, h := range handlers { + if h.node != nil && h.node.Step.ID != "" && h.node.Step.ID == ref { + return h.node + } + } + for _, h := range handlers { + if h.node != nil && h.key == ref { + return h.node + } + } + return nil +} + +func toStepOutputs(node *ir.Node) api.StepOutputs { + data := runtimepkg.NodeData{ + Step: node.Step, + State: runtimepkg.NodeState{ + OutputValue: node.OutputValue, + OutputVariables: node.OutputVariables, + OutputsValue: node.OutputsValue, + StepOutputsValue: node.StepOutputsValue, + }, + } + + // As in a ${steps..outputs.} reference, the published payload + // stands in for a step that has no declared outputs. + values := data.StepOutputsValueMap() + if node.StepOutputsValue == nil { + values = data.OutputsValueStringMap() + } + names := make([]string, 0, len(values)) + for name := range values { + names = append(names, name) + } + sort.Strings(names) + outputs := make([]api.StepOutputValue, 0, len(names)) + for _, name := range names { + outputs = append(outputs, toStepOutputValue(name, values[name])) + } + + result := api.StepOutputs{ + StepName: node.Step.Name, + Status: api.NodeStatus(node.Status), + StatusLabel: api.NodeStatusLabel(node.Status.String()), + Outputs: outputs, + } + if node.Step.ID != "" { + result.StepId = ptrOf(node.Step.ID) + } + if value, ok := outputVariableValue(data); ok { + v := toStepOutputValue(node.Step.Output, value) + result.OutputVariable = &v + } + return result +} + +// outputVariableValue returns the value that ${NAME} gives for a step with +// output: NAME. The captured variable comes first: with output_schema, the +// output value holds a normalized copy of it. +func outputVariableValue(data runtimepkg.NodeData) (string, bool) { + if vars := data.State.OutputVariables; data.Step.Output != "" && vars != nil { + if raw, ok := vars.Load(data.Step.Output); ok { + if entry, ok := raw.(string); ok { + if _, value, found := strings.Cut(entry, "="); found { + return value, true + } + } + } + } + return data.StringFormOutputValue() +} + +// toStepOutputValue cuts value at maxStepOutputBytes without splitting a rune. +func toStepOutputValue(name, value string) api.StepOutputValue { + size := len(value) + if size <= maxStepOutputBytes { + return api.StepOutputValue{Name: name, Value: value, Size: size} + } + cut := maxStepOutputBytes + for cut > 0 && !utf8.RuneStart(value[cut]) { + cut-- + } + return api.StepOutputValue{Name: name, Value: value[:cut], Size: size, Truncated: true} +} diff --git a/internal/service/frontend/api/v1/editor_step_outputs_test.go b/internal/service/frontend/api/v1/editor_step_outputs_test.go new file mode 100644 index 0000000000..7f5d462002 --- /dev/null +++ b/internal/service/frontend/api/v1/editor_step_outputs_test.go @@ -0,0 +1,416 @@ +// Copyright (C) 2026 Yota Hamada +// SPDX-License-Identifier: GPL-3.0-or-later + +package api + +import ( + "encoding/json" + "strings" + "testing" + "time" + "unicode/utf8" + + openapiv1 "github.com/dagucloud/dagu/v2/api/v1" + "github.com/dagucloud/dagu/v2/internal/cmn/collections" + "github.com/dagucloud/dagu/v2/internal/cmn/config" + "github.com/dagucloud/dagu/v2/internal/cmn/masking" + "github.com/dagucloud/dagu/v2/internal/ir" + "github.com/dagucloud/dagu/v2/internal/persis" + runtimepkg "github.com/dagucloud/dagu/v2/internal/runtime" + "github.com/dagucloud/dagu/v2/internal/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newStepOutputsAPI(t *testing.T, nodes ...*ir.Node) *API { + t.Helper() + repository := testutil.NewFileDAGRunRepository(t.TempDir(), persis.DAGRunRepositoryOptions{}) + ctx := t.Context() + dag := &ir.DAG{Name: "demo"} + attempt, err := repository.CreateAttempt(ctx, dag, time.Now().UTC(), "run-1", persis.DAGRunCreateAttemptOptions{}) + require.NoError(t, err) + require.NoError(t, attempt.Open(ctx)) + status := ir.InitialStatus(dag) + status.DAGRunID = "run-1" + status.AttemptID = attempt.ID() + status.Status = ir.Succeeded + status.Nodes = nodes + require.NoError(t, attempt.Write(ctx, status)) + require.NoError(t, attempt.Close(ctx)) + cfg := &config.Config{} + return &API{ + dagRunRepository: repository, + dagRunMgr: runtimepkg.NewManager(repository, nil, cfg), + config: cfg, + } +} + +func stepOutputsNode(name, id string, outputs map[string]any) *ir.Node { + raw, _ := json.Marshal(outputs) + s := string(raw) + return &ir.Node{Step: ir.Step{Name: name, ID: id}, Status: ir.NodeSucceeded, StepOutputsValue: &s} +} + +func getStepOutputs(t *testing.T, a *API, runID, step string) openapiv1.GetDAGRunStepOutputsResponseObject { + t.Helper() + resp, err := a.GetDAGRunStepOutputs(t.Context(), openapiv1.GetDAGRunStepOutputsRequestObject{ + Name: "demo", DagRunId: runID, StepName: step, + }) + require.NoError(t, err) + return resp +} + +func requireStepOutputs(t *testing.T, resp openapiv1.GetDAGRunStepOutputsResponseObject) openapiv1.StepOutputs { + t.Helper() + ok, is := resp.(openapiv1.GetDAGRunStepOutputs200JSONResponse) + require.True(t, is, "got %T", resp) + return openapiv1.StepOutputs(ok) +} + +func TestStepOutputsByName(t *testing.T) { + node := stepOutputsNode("build", "b1", map[string]any{"zeta": "z", "alpha": 3}) + vars := &collections.SyncMap{} + vars.Store("RESULT", "RESULT=hello") + node.Step.Output = "RESULT" + node.OutputVariables = vars + a := newStepOutputsAPI(t, node) + + got := requireStepOutputs(t, getStepOutputs(t, a, "latest", "build")) + assert.Equal(t, "build", got.StepName) + require.NotNil(t, got.StepId) + assert.Equal(t, "b1", *got.StepId) + assert.Equal(t, openapiv1.NodeStatus(ir.NodeSucceeded), got.Status) + assert.Equal(t, openapiv1.NodeStatusLabel(ir.NodeSucceeded.String()), got.StatusLabel) + require.Len(t, got.Outputs, 2) + assert.Equal(t, "alpha", got.Outputs[0].Name) + assert.Equal(t, "3", got.Outputs[0].Value) + assert.Equal(t, "zeta", got.Outputs[1].Name) + require.NotNil(t, got.OutputVariable) + assert.Equal(t, "RESULT", got.OutputVariable.Name) + assert.Equal(t, "hello", got.OutputVariable.Value) +} + +// An executor that publishes outputs without declaring them fills only the +// channel that ${steps..outputs.} falls back to. Its outputs show, +// unless the step has declared outputs, which win as they do in a reference. +func TestStepOutputsFromPublishedPayload(t *testing.T) { + payload := `{"rows":3,"name":"x"}` + published := &ir.Node{Step: ir.Step{Name: "load"}, Status: ir.NodeSucceeded, OutputsValue: &payload} + declared := stepOutputsNode("both", "", map[string]any{"a": "1"}) + declared.OutputsValue = &payload + a := newStepOutputsAPI(t, published, declared) + + got := requireStepOutputs(t, getStepOutputs(t, a, "run-1", "load")) + assert.Equal(t, []openapiv1.StepOutputValue{ + {Name: "name", Value: "x", Size: 1}, + {Name: "rows", Value: "3", Size: 1}, + }, got.Outputs) + + got = requireStepOutputs(t, getStepOutputs(t, a, "run-1", "both")) + assert.Equal(t, []openapiv1.StepOutputValue{{Name: "a", Value: "1", Size: 1}}, got.Outputs) +} + +// The output variable is the value that ${NAME} gives, the captured stdout, +// also when output_schema keeps a normalized copy as the output value. +func TestStepOutputsVariableIsCaptured(t *testing.T) { + normalized := `{"a":1}` + vars := &collections.SyncMap{} + vars.Store("RESULT", `RESULT={ "a": 1 }`) + node := &ir.Node{ + Step: ir.Step{Name: "build", Output: "RESULT"}, + Status: ir.NodeSucceeded, + OutputValue: &normalized, + OutputVariables: vars, + } + a := newStepOutputsAPI(t, node) + + got := requireStepOutputs(t, getStepOutputs(t, a, "run-1", "build")) + require.NotNil(t, got.OutputVariable) + assert.Equal(t, "RESULT", got.OutputVariable.Name) + assert.Equal(t, `{ "a": 1 }`, got.OutputVariable.Value) +} + +func TestStepOutputsByID(t *testing.T) { + a := newStepOutputsAPI(t, stepOutputsNode("build", "b1", map[string]any{"k": "v"})) + + got := requireStepOutputs(t, getStepOutputs(t, a, "run-1", "b1")) + assert.Equal(t, "build", got.StepName) + require.Len(t, got.Outputs, 1) + assert.Equal(t, "v", got.Outputs[0].Value) +} + +func TestStepOutputsEmpty(t *testing.T) { + a := newStepOutputsAPI(t, &ir.Node{Step: ir.Step{Name: "quiet"}, Status: ir.NodeNotStarted}) + + got := requireStepOutputs(t, getStepOutputs(t, a, "run-1", "quiet")) + assert.NotNil(t, got.Outputs) + assert.Empty(t, got.Outputs) + assert.Nil(t, got.OutputVariable) + assert.Nil(t, got.StepId) +} + +func TestStepOutputsUnknown(t *testing.T) { + a := newStepOutputsAPI(t, stepOutputsNode("build", "", nil)) + + resp := getStepOutputs(t, a, "run-1", "nope") + nf, ok := resp.(openapiv1.GetDAGRunStepOutputs404JSONResponse) + require.True(t, ok, "got %T", resp) + assert.Equal(t, openapiv1.ErrorCodeNotFound, nf.Code) + + resp = getStepOutputs(t, a, "missing-run", "build") + nf, ok = resp.(openapiv1.GetDAGRunStepOutputs404JSONResponse) + require.True(t, ok, "got %T", resp) + assert.Equal(t, openapiv1.ErrorCodeNotFound, nf.Code) +} + +func TestStepOutputsTruncated(t *testing.T) { + // "é" has two bytes, so an odd cut point falls inside a rune. + big := "a" + strings.Repeat("é", maxStepOutputBytes) + small := strings.Repeat("x", 10) + a := newStepOutputsAPI(t, stepOutputsNode("build", "", map[string]any{"big": big, "small": small})) + + got := requireStepOutputs(t, getStepOutputs(t, a, "run-1", "build")) + require.Len(t, got.Outputs, 2) + bigOut, smallOut := got.Outputs[0], got.Outputs[1] + assert.True(t, bigOut.Truncated) + assert.Equal(t, len(big), bigOut.Size) + assert.True(t, utf8.ValidString(bigOut.Value)) + assert.LessOrEqual(t, len(bigOut.Value), maxStepOutputBytes) + assert.Equal(t, maxStepOutputBytes-1, len(bigOut.Value)) + assert.False(t, smallOut.Truncated) + assert.Equal(t, small, smallOut.Value) + assert.Equal(t, 10, smallOut.Size) +} + +func TestStepOutputsMasked(t *testing.T) { + a := newStepOutputsAPI(t, stepOutputsNode("build", "", map[string]any{"token": "*******"})) + + got := requireStepOutputs(t, getStepOutputs(t, a, "run-1", "build")) + require.Len(t, got.Outputs, 1) + assert.Equal(t, "*******", got.Outputs[0].Value) +} + +// updateStepOutputsStatus rewrites the latest persisted status of the demo DAG. +func updateStepOutputsStatus(t *testing.T, a *API, mutate func(*ir.DAGRunStatus)) { + t.Helper() + ctx := t.Context() + attempt, err := a.dagRunRepository.LatestAttempt(ctx, "demo", persis.DAGRunLatestAttemptOptions{}) + require.NoError(t, err) + status, err := attempt.ReadStatus(ctx) + require.NoError(t, err) + mutate(status) + require.NoError(t, attempt.Open(ctx)) + require.NoError(t, attempt.Write(ctx, *status)) + require.NoError(t, attempt.Close(ctx)) +} + +func TestStepOutputsPrefersTopLevel(t *testing.T) { + a := newStepOutputsAPI(t, stepOutputsNode("same", "", map[string]any{"from": "top"})) + // A handler with the same name must not win over the top-level step. + updateStepOutputsStatus(t, a, func(s *ir.DAGRunStatus) { + s.OnExit = stepOutputsNode("same", "", map[string]any{"from": "handler"}) + }) + + got := requireStepOutputs(t, getStepOutputs(t, a, "run-1", "same")) + require.Len(t, got.Outputs, 1) + assert.Equal(t, "top", got.Outputs[0].Value) +} + +// A top-level step id wins over a handler step name. +func TestStepOutputsTopLevelIDBeforeHandlerName(t *testing.T) { + a := newStepOutputsAPI(t, stepOutputsNode("build", "onExit", map[string]any{"from": "top"})) + updateStepOutputsStatus(t, a, func(s *ir.DAGRunStatus) { + s.OnExit = stepOutputsNode("onExit", "", map[string]any{"from": "handler"}) + }) + + got := requireStepOutputs(t, getStepOutputs(t, a, "run-1", "onExit")) + assert.Equal(t, "build", got.StepName) + require.Len(t, got.Outputs, 1) + assert.Equal(t, "top", got.Outputs[0].Value) +} + +func TestStepOutputsHandlerKeys(t *testing.T) { + a := newStepOutputsAPI(t, stepOutputsNode("build", "", nil)) + updateStepOutputsStatus(t, a, func(s *ir.DAGRunStatus) { + s.OnInit = stepOutputsNode("onInit", "", map[string]any{"from": "init"}) + s.OnSuccess = stepOutputsNode("onSuccess", "", map[string]any{"from": "success"}) + s.OnFailure = stepOutputsNode("onFailure", "", map[string]any{"from": "failure"}) + s.OnAbort = stepOutputsNode("onAbort", "", map[string]any{"from": "abort"}) + s.OnExit = stepOutputsNode("onExit", "exit-id", map[string]any{"from": "exit"}) + s.OnWait = stepOutputsNode("onWait", "", map[string]any{"from": "wait"}) + }) + + tests := []struct{ ref, want string }{ + {"handler_on.init", "init"}, + {"handler_on.success", "success"}, + {"handler_on.failure", "failure"}, + {"handler_on.abort", "abort"}, + {"handler_on.exit", "exit"}, + {"handler_on.wait", "wait"}, + {"onFailure", "failure"}, + {"exit-id", "exit"}, + } + for _, tt := range tests { + t.Run(tt.ref, func(t *testing.T) { + got := requireStepOutputs(t, getStepOutputs(t, a, "run-1", tt.ref)) + require.Len(t, got.Outputs, 1) + assert.Equal(t, tt.want, got.Outputs[0].Value) + }) + } + + resp := getStepOutputs(t, a, "run-1", "handler_on.other") + _, ok := resp.(openapiv1.GetDAGRunStepOutputs404JSONResponse) + assert.True(t, ok, "got %T", resp) +} + +// A handler key of an event without a handler step is not found. +func TestStepOutputsHandlerKeyAbsent(t *testing.T) { + a := newStepOutputsAPI(t, stepOutputsNode("build", "", nil)) + + resp := getStepOutputs(t, a, "run-1", "handler_on.failure") + _, ok := resp.(openapiv1.GetDAGRunStepOutputs404JSONResponse) + assert.True(t, ok, "got %T", resp) +} + +// addStepOutputsRun persists one more run of the demo DAG that starts at the given time. +func addStepOutputsRun(t *testing.T, a *API, runID string, at time.Time, nodes ...*ir.Node) { + t.Helper() + ctx := t.Context() + dag := &ir.DAG{Name: "demo"} + attempt, err := a.dagRunRepository.CreateAttempt(ctx, dag, at, runID, persis.DAGRunCreateAttemptOptions{}) + require.NoError(t, err) + require.NoError(t, attempt.Open(ctx)) + status := ir.InitialStatus(dag) + status.DAGRunID = runID + status.AttemptID = attempt.ID() + status.Status = ir.Succeeded + status.Nodes = nodes + require.NoError(t, attempt.Write(ctx, status)) + require.NoError(t, attempt.Close(ctx)) +} + +func TestStepOutputsFailedNode(t *testing.T) { + node := stepOutputsNode("build", "", map[string]any{"partial": "yes"}) + node.Status = ir.NodeFailed + a := newStepOutputsAPI(t, node) + + got := requireStepOutputs(t, getStepOutputs(t, a, "run-1", "build")) + assert.Equal(t, openapiv1.NodeStatus(ir.NodeFailed), got.Status) + assert.Equal(t, openapiv1.NodeStatusLabel("failed"), got.StatusLabel) + require.Len(t, got.Outputs, 1) + assert.Equal(t, "yes", got.Outputs[0].Value) +} + +func TestStepOutputsLatestRun(t *testing.T) { + a := newStepOutputsAPI(t, stepOutputsNode("build", "", map[string]any{"run": "first"})) + addStepOutputsRun(t, a, "run-2", time.Now().UTC().Add(time.Hour), + stepOutputsNode("build", "", map[string]any{"run": "second"})) + + got := requireStepOutputs(t, getStepOutputs(t, a, "latest", "build")) + require.Len(t, got.Outputs, 1) + assert.Equal(t, "second", got.Outputs[0].Value) + + got = requireStepOutputs(t, getStepOutputs(t, a, "run-1", "build")) + require.Len(t, got.Outputs, 1) + assert.Equal(t, "first", got.Outputs[0].Value) +} + +// Foreach and sub-DAG steps return the values of the parent node only. The +// children name sub-run ids; the handler never reads a run for them. +func TestStepOutputsParentOnly(t *testing.T) { + children := []ir.SubDAGRun{ + {DAGRunID: "sub-1", DAGName: "child-dag", ParallelItem: "a"}, + {DAGRunID: "sub-2", DAGName: "child-dag", ParallelItem: "b"}, + } + foreach := stepOutputsNode("each", "", map[string]any{"parent": "p"}) + foreach.Step.Foreach = &ir.ForeachConfig{} + foreach.SubRuns = children + foreach.SubRunsRepeated = children[:1] + subDAG := stepOutputsNode("call", "", map[string]any{"parent": "p"}) + subDAG.Step.SubDAG = &ir.SubDAG{Name: "child-dag"} + subDAG.SubRuns = children[:1] + + a := newStepOutputsAPI(t, foreach, subDAG) + // A run with the id of a child, holding steps of the same names, stays unused. + addStepOutputsRun(t, a, "sub-1", time.Now().UTC().Add(-time.Hour), + stepOutputsNode("each", "", map[string]any{"child": "c"}), + stepOutputsNode("call", "", map[string]any{"child": "c"})) + + for _, name := range []string{"each", "call"} { + t.Run(name, func(t *testing.T) { + got := requireStepOutputs(t, getStepOutputs(t, a, "run-1", name)) + assert.Equal(t, name, got.StepName) + require.Len(t, got.Outputs, 1) + assert.Equal(t, "parent", got.Outputs[0].Name) + assert.Equal(t, "p", got.Outputs[0].Value) + }) + } +} + +// The runtime masks node outputs with a masker built from the secret +// environment before it persists them (internal/runtime/agent/status_masking.go). +// The test applies the same masker to the same node fields, persists the result, +// and checks that the endpoint shows the masked text only. +func TestStepOutputsMaskedBeforePersistence(t *testing.T) { + const secret = "s3cr3t-token-value" + masker := masking.NewMasker(masking.SourcedEnvVars{Secrets: []string{"API_TOKEN=" + secret}}) + + node := stepOutputsNode("build", "", map[string]any{"url": "https://host/?token=" + secret}) + node.Step.Output = "RESULT" + vars := &collections.SyncMap{} + vars.Store("RESULT", masker.MaskString("RESULT=value-"+secret)) + node.OutputVariables = vars + value := masker.MaskString("value-" + secret) + node.OutputValue = &value + outputs := masker.MaskString(*node.StepOutputsValue) + node.StepOutputsValue = &outputs + + a := newStepOutputsAPI(t, node) + got := requireStepOutputs(t, getStepOutputs(t, a, "latest", "build")) + + raw, err := json.Marshal(got) + require.NoError(t, err) + assert.NotContains(t, string(raw), secret) + require.Len(t, got.Outputs, 1) + assert.Equal(t, "https://host/?token="+masking.DefaultMaskString, got.Outputs[0].Value) + require.NotNil(t, got.OutputVariable) + assert.Equal(t, "value-"+masking.DefaultMaskString, got.OutputVariable.Value) +} + +func TestStepOutputsSizeLimit(t *testing.T) { + limit := maxStepOutputBytes + tests := []struct { + name string + value string + wantLen int + truncated bool + }{ + {"exact", strings.Repeat("a", limit), limit, false}, + {"over", strings.Repeat("a", limit+1), limit, true}, + // "€" has three bytes; the limit falls after the first byte of a rune. + {"rune3", strings.Repeat("a", limit-1) + "€", limit - 1, true}, + // "😀" has four bytes; the limit falls after the third byte of a rune. + {"rune4", strings.Repeat("a", limit-3) + "😀", limit - 3, true}, + // A rune that ends exactly at the limit stays whole. + {"rune-fits", strings.Repeat("a", limit-3) + "€" + "b", limit, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + node := stepOutputsNode("build", "", map[string]any{"v": tt.value}) + node.Step.Output = "RESULT" + node.OutputValue = &tt.value + a := newStepOutputsAPI(t, node) + + got := requireStepOutputs(t, getStepOutputs(t, a, "run-1", "build")) + require.Len(t, got.Outputs, 1) + require.NotNil(t, got.OutputVariable) + for _, v := range []openapiv1.StepOutputValue{got.Outputs[0], *got.OutputVariable} { + assert.Equal(t, len(tt.value), v.Size) + assert.Equal(t, tt.truncated, v.Truncated) + assert.Len(t, v.Value, tt.wantLen) + assert.True(t, utf8.ValidString(v.Value)) + assert.True(t, strings.HasPrefix(tt.value, v.Value)) + } + }) + } +} diff --git a/internal/service/frontend/api/v1/seeded_run.go b/internal/service/frontend/api/v1/seeded_run.go index 62a872eb1b..09f9397b62 100644 --- a/internal/service/frontend/api/v1/seeded_run.go +++ b/internal/service/frontend/api/v1/seeded_run.go @@ -19,15 +19,19 @@ import ( // seededRun describes a new DAG-run whose node states are fixed before it // starts, so only the steps left not started execute. type seededRun struct { - dag *ir.DAG - dagRunID string - nodes []runtime.NodeData - source *ir.DAGRunStatus - params string - profileName string - labels string - noReuse bool - triggerType ir.TriggerType + dag *ir.DAG + dagRunID string + nodes []runtime.NodeData + reused []ir.ReusedInput + // skipHandlers marks a run that does not call the end-of-run handlers. + skipHandlers bool + source *ir.DAGRunStatus + params string + profileName string + labels string + noReuse bool + ephemeral bool + triggerType ir.TriggerType // enqueue sends the run to the DAG's queue when one is configured. enqueue bool } @@ -46,12 +50,15 @@ func (a *API) launchSeededDAGRun(ctx context.Context, run seededRun) (queued boo DAG: run.dag, DAGRunID: run.dagRunID, Nodes: run.nodes, + ReusedInputs: run.reused, + SkipHandlers: run.skipHandlers, Source: run.source, Params: run.params, TriggerType: run.triggerType, TriggerActor: triggerActorFromContext(ctx), ProfileName: run.profileName, NoReuse: run.noReuse, + Ephemeral: run.ephemeral, LogBaseDir: a.config.Paths.LogDir, ArtifactBaseDir: a.config.Paths.ArtifactDir, }) diff --git a/internal/service/frontend/api/v1/start_steps.go b/internal/service/frontend/api/v1/start_steps.go index 782b8e20b6..e0e0edaca9 100644 --- a/internal/service/frontend/api/v1/start_steps.go +++ b/internal/service/frontend/api/v1/start_steps.go @@ -10,6 +10,7 @@ import ( "net/http" "slices" "strings" + "time" api "github.com/dagucloud/dagu/v2/api/v1" "github.com/dagucloud/dagu/v2/internal/dagrun" @@ -37,21 +38,38 @@ type selectedStepsStart struct { type stepSelection struct { steps []string outputsFrom string + // outputsFromRuns names, by step, the run that the step carries its + // outputs from instead of outputsFrom. + outputsFromRuns map[string]string // outputs are outputs of skipped steps, by step and then output name. outputs map[string]map[string]string + // outputsSources names, by step, the run each step of outputs was captured from. + outputsSources map[string]intake.OutputSource + // ephemeral runs leave no trace: the run is stored apart from the history, + // listed nowhere, and removed shortly after it ends. + ephemeral bool } // selectedStepsFromBody validates the step selection fields of a start // request body. It returns no steps when the request runs the whole DAG. -func selectedStepsFromBody(steps *[]string, outputsFromRunID *string, outputs *map[string]map[string]string) (stepSelection, error) { +func selectedStepsFromBody(steps *[]string, outputsFromRunID *string, outputsFromRuns *map[string]string, outputs *map[string]map[string]string, outputsSources *map[string]api.ReusedOutputSource, ephemeral *bool) (stepSelection, error) { outputsFrom := strings.TrimSpace(valueOf(outputsFromRunID)) if steps == nil { + if valueOf(ephemeral) { + return stepSelection{}, badSelectedStepsRequest("ephemeral requires steps: a run that leaves no trace is for steps") + } if outputsFrom != "" { return stepSelection{}, badSelectedStepsRequest("outputsFromRunId requires steps") } + if outputsFromRuns != nil { + return stepSelection{}, badSelectedStepsRequest("outputsFromRuns requires steps") + } if outputs != nil { return stepSelection{}, badSelectedStepsRequest("outputs requires steps") } + if outputsSources != nil { + return stepSelection{}, badSelectedStepsRequest("outputsSources requires steps") + } return stepSelection{}, nil } if len(*steps) == 0 { @@ -60,7 +78,50 @@ func selectedStepsFromBody(steps *[]string, outputsFromRunID *string, outputs *m if err := validateDAGRunID(outputsFrom); err != nil { return stepSelection{}, err } - return stepSelection{steps: *steps, outputsFrom: outputsFrom, outputs: valueOf(outputs)}, nil + fromRuns := make(map[string]string, len(valueOf(outputsFromRuns))) + for step, runID := range valueOf(outputsFromRuns) { + runID = strings.TrimSpace(runID) + if runID == "" { + return stepSelection{}, badSelectedStepsRequest(fmt.Sprintf("outputsFromRuns for step %s must name a run", step)) + } + if err := validateDAGRunID(runID); err != nil { + return stepSelection{}, err + } + fromRuns[step] = runID + } + sources, err := outputSourcesFromBody(valueOf(outputsSources)) + if err != nil { + return stepSelection{}, err + } + return stepSelection{steps: *steps, outputsFrom: outputsFrom, outputsFromRuns: fromRuns, outputs: valueOf(outputs), outputsSources: sources, ephemeral: valueOf(ephemeral)}, nil +} + +// outputSourcesFromBody validates the run IDs and times of outputsSources. A +// source run need not exist: the outputs may come from a run that is gone. +func outputSourcesFromBody(body map[string]api.ReusedOutputSource) (map[string]intake.OutputSource, error) { + if len(body) == 0 { + return nil, nil + } + sources := make(map[string]intake.OutputSource, len(body)) + for step, source := range body { + runID := strings.TrimSpace(source.RunId) + if runID == "" { + return nil, badSelectedStepsRequest(fmt.Sprintf("outputsSources for step %s must name a runId", step)) + } + if err := validateDAGRunID(runID); err != nil { + return nil, err + } + entry := intake.OutputSource{RunID: runID} + if capturedAt := strings.TrimSpace(valueOf(source.CapturedAt)); capturedAt != "" { + t, err := time.Parse(time.RFC3339, capturedAt) + if err != nil { + return nil, badSelectedStepsRequest(fmt.Sprintf("outputsSources for step %s: capturedAt must be an RFC 3339 timestamp", step)) + } + entry.CapturedAt = t.Format(time.RFC3339) + } + sources[step] = entry + } + return sources, nil } // startSelectedSteps starts a new run of dag in which only the selected steps @@ -100,21 +161,36 @@ func (a *API) startSelectedSteps(ctx context.Context, dag *ir.DAG, req selectedS return err } } - nodes, err := intake.SelectedStepNodes(dag, req.selection.steps, source, req.selection.outputs) + fromRuns, err := a.selectedStepsFromRuns(ctx, dag.Name, req.selection.outputsFromRuns) + if err != nil { + return err + } + if err := intake.CheckOutputSources(dag, req.selection.outputs, req.selection.outputsSources); err != nil { + return badSelectedStepsRequest(err.Error()) + } + nodes, reused, err := intake.SelectedStepNodes(dag, req.selection.steps, source, fromRuns, req.selection.outputs, req.selection.outputsSources) + if err != nil { + return badSelectedStepsRequest(err.Error()) + } + + skipHandlers, err := intake.SelectionSkipsHandlers(dag, req.selection.steps) if err != nil { return badSelectedStepsRequest(err.Error()) } _, err = a.launchSeededDAGRun(ctx, seededRun{ - dag: dag, - dagRunID: req.dagRunID, - nodes: nodes, - source: source, - params: req.params, - profileName: req.profileName, - labels: req.labels, - noReuse: req.noReuse, - triggerType: ir.TriggerTypeManual, + dag: dag, + dagRunID: req.dagRunID, + nodes: nodes, + reused: reused, + skipHandlers: skipHandlers, + source: source, + params: req.params, + profileName: req.profileName, + labels: req.labels, + noReuse: req.noReuse, + ephemeral: req.selection.ephemeral, + triggerType: ir.TriggerTypeManual, }) return err } @@ -142,17 +218,44 @@ func (a *API) selectedStepsSource(ctx context.Context, dagName, dagRunID string) return status, nil } +// selectedStepsFromRuns reads the run that each step of outputsFromRuns carries +// its outputs from. A run that does not exist for the DAG (it was removed, or +// belongs to another DAG) is a bad request that names the step and the run. +func (a *API) selectedStepsFromRuns(ctx context.Context, dagName string, refs map[string]string) (intake.RunStepSources, error) { + runs, err := intake.LoadRunStepSources(ctx, a.dagRunRepository, dagName, refs, func(status *ir.DAGRunStatus) error { + return a.requireDAGRunStatusExecute(ctx, status) + }) + if sourceErr, ok := errors.AsType[*intake.RunStepSourceError](err); ok { + return nil, badSelectedStepsRequest(sourceErr.Message) + } + return runs, err +} + // addSelectedStepsAudit records the step selection of a start request. Only // the names of supplied outputs are recorded, since their values may be -// credentials. +// credentials. The request is recorded as ephemeral when the run leaves no +// trace, so the audit log stays the one record of it. func addSelectedStepsAudit(details map[string]any, selection stepSelection) { if len(selection.steps) == 0 { return } details["steps"] = selection.steps + if selection.ephemeral { + details["ephemeral"] = true + } if selection.outputsFrom != "" { details["outputs_from_dag_run_id"] = selection.outputsFrom } + if len(selection.outputsFromRuns) > 0 { + details["outputs_from_runs"] = selection.outputsFromRuns + } + if len(selection.outputsSources) > 0 { + sources := make(map[string]string, len(selection.outputsSources)) + for step, source := range selection.outputsSources { + sources[step] = source.RunID + } + details["outputs_sources"] = sources + } if len(selection.outputs) > 0 { var names []string for step, values := range selection.outputs { @@ -172,3 +275,16 @@ func badSelectedStepsRequest(message string) *Error { Message: message, } } + +// rejectEphemeralRun refuses an action that would carry an ephemeral run into +// the history or run it again. +func rejectEphemeralRun(status *ir.DAGRunStatus, action string) error { + if status == nil || !status.Ephemeral { + return nil + } + return &Error{ + HTTPStatus: http.StatusConflict, + Code: api.ErrorCodeConflict, + Message: fmt.Sprintf("dag-run %s is ephemeral and cannot be %s", status.DAGRun(), action), + } +} diff --git a/internal/service/frontend/api/v1/start_steps_internal_test.go b/internal/service/frontend/api/v1/start_steps_internal_test.go index 05d74da0a1..8dbd4da1fc 100644 --- a/internal/service/frontend/api/v1/start_steps_internal_test.go +++ b/internal/service/frontend/api/v1/start_steps_internal_test.go @@ -6,11 +6,16 @@ package api import ( "context" "net/http" + "path/filepath" "testing" + gen "github.com/dagucloud/dagu/v2/api/v1" "github.com/dagucloud/dagu/v2/internal/dispatch" + "github.com/dagucloud/dagu/v2/internal/intake" "github.com/dagucloud/dagu/v2/internal/ir" + "github.com/dagucloud/dagu/v2/internal/persis" "github.com/dagucloud/dagu/v2/internal/spec" + runtestutil "github.com/dagucloud/dagu/v2/internal/testutil" "github.com/stretchr/testify/require" ) @@ -53,6 +58,13 @@ func TestStartStepsDispatchesSeed(t *testing.T) { require.Equal(t, "RESULT=from-source", raw) require.Equal(t, ir.NodeNotStarted, status.Nodes[1].Status) require.Equal(t, ir.NodeSkipped, status.Nodes[2].Status) + + // Only the skipped step that carried outputs has a provenance. + require.Len(t, status.ReusedInputs, 1) + require.Equal(t, "build", status.ReusedInputs[0].Step) + require.Equal(t, "source-run", status.ReusedInputs[0].RunID) + require.Equal(t, ir.ReusedInputViaRun, status.ReusedInputs[0].Via) + require.Equal(t, status.Nodes[0].FinishedAt, status.ReusedInputs[0].CapturedAt) } // Supplied outputs are seeded onto the skipped step, replacing the value the @@ -67,9 +79,10 @@ func TestStartStepsSetsOutputs(t *testing.T) { err = api.startSelectedSteps(ctx, dag, selectedStepsStart{ selection: stepSelection{ - steps: []string{"consume"}, - outputsFrom: "source-run", - outputs: map[string]map[string]string{"build": {"RESULT": "given"}}, + steps: []string{"consume"}, + outputsFrom: "source-run", + outputs: map[string]map[string]string{"build": {"RESULT": "given"}}, + outputsSources: map[string]intake.OutputSource{"build": {RunID: "captured-run", CapturedAt: "2026-10-26T10:00:00Z"}}, }, dagRunID: "only-run", }) @@ -82,6 +95,11 @@ func TestStartStepsSetsOutputs(t *testing.T) { raw, ok := status.Nodes[0].OutputVariables.Load("RESULT") require.True(t, ok) require.Equal(t, "RESULT=given", raw) + + // The supplied step names its run; the carried status keeps it. + require.Equal(t, []ir.ReusedInput{ + {Step: "build", RunID: "captured-run", CapturedAt: "2026-10-26T10:00:00Z", Via: ir.ReusedInputViaCache}, + }, status.ReusedInputs) } func TestStartStepsRejects(t *testing.T) { @@ -106,15 +124,31 @@ func TestStartStepsRejects(t *testing.T) { wantStatus: http.StatusNotFound, wantMsg: "dag-run nope not found", }, + { + name: "RunOfAStepIsGone", + req: selectedStepsStart{selection: stepSelection{steps: []string{"consume"}, outputsFromRuns: map[string]string{"build": "nope"}}, dagRunID: "run"}, + wantStatus: http.StatusBadRequest, + wantMsg: "step build cannot be reused from run nope: the run does not exist for DAG", + }, { name: "OutputOfSelectedStep", req: selectedStepsStart{selection: stepSelection{ - steps: []string{"consume"}, - outputs: map[string]map[string]string{"consume": {"RESULT": "x"}}, + steps: []string{"consume"}, + outputs: map[string]map[string]string{"consume": {"RESULT": "x"}}, + outputsSources: map[string]intake.OutputSource{"consume": {RunID: "captured-run"}}, }, dagRunID: "run"}, wantStatus: http.StatusBadRequest, wantMsg: `cannot set outputs of step "consume": it is selected to run`, }, + { + name: "SourceWithoutOutput", + req: selectedStepsStart{selection: stepSelection{ + steps: []string{"consume"}, + outputsSources: map[string]intake.OutputSource{"build": {RunID: "captured-run"}}, + }, dagRunID: "run"}, + wantStatus: http.StatusBadRequest, + wantMsg: "outputsSources names step build, which is not in outputs", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -127,18 +161,148 @@ func TestStartStepsRejects(t *testing.T) { } t.Run("OutputsFromNeedsSteps", func(t *testing.T) { - _, err := selectedStepsFromBody(nil, ptrOf("source"), nil) + _, err := selectedStepsFromBody(nil, ptrOf("source"), nil, nil, nil, nil) var apiErr *Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusBadRequest, apiErr.HTTPStatus) require.Contains(t, apiErr.Message, "outputsFromRunId requires steps") }) + t.Run("OutputsFromRunsNeedSteps", func(t *testing.T) { + _, err := selectedStepsFromBody(nil, nil, &map[string]string{"build": "run-1"}, nil, nil, nil) + var apiErr *Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusBadRequest, apiErr.HTTPStatus) + require.Contains(t, apiErr.Message, "outputsFromRuns requires steps") + }) + + t.Run("OutputsFromRunsRunIDs", func(t *testing.T) { + steps := []string{"consume"} + for name, runID := range map[string]string{"Empty": " ", "BadID": "bad/id"} { + _, err := selectedStepsFromBody(&steps, nil, &map[string]string{"build": runID}, nil, nil, nil) + var apiErr *Error + require.ErrorAs(t, err, &apiErr, name) + require.Equal(t, http.StatusBadRequest, apiErr.HTTPStatus, name) + } + got, err := selectedStepsFromBody(&steps, nil, &map[string]string{"build": " run-1 "}, nil, nil, nil) + require.NoError(t, err) + require.Equal(t, map[string]string{"build": "run-1"}, got.outputsFromRuns) + }) + + t.Run("OutputsSourcesNeedSteps", func(t *testing.T) { + _, err := selectedStepsFromBody(nil, nil, nil, nil, &map[string]gen.ReusedOutputSource{"build": {RunId: "run-1"}}, nil) + var apiErr *Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusBadRequest, apiErr.HTTPStatus) + require.Contains(t, apiErr.Message, "outputsSources requires steps") + }) + + t.Run("SourceRunID", func(t *testing.T) { + steps := []string{"consume"} + for name, source := range map[string]gen.ReusedOutputSource{ + "Empty": {}, + "BadID": {RunId: "bad/id"}, + "BadCapture": {RunId: "run-1", CapturedAt: ptrOf("yesterday")}, + } { + _, err := selectedStepsFromBody(&steps, nil, nil, nil, &map[string]gen.ReusedOutputSource{"build": source}, nil) + var apiErr *Error + require.ErrorAs(t, err, &apiErr, name) + require.Equal(t, http.StatusBadRequest, apiErr.HTTPStatus, name) + } + + // A run that does not exist is a valid source. + got, err := selectedStepsFromBody(&steps, nil, nil, nil, &map[string]gen.ReusedOutputSource{ + "build": {RunId: "gone-run", CapturedAt: ptrOf("2026-10-26T12:00:00+02:00")}, + }, nil) + require.NoError(t, err) + require.Equal(t, intake.OutputSource{RunID: "gone-run", CapturedAt: "2026-10-26T12:00:00+02:00"}, got.outputsSources["build"]) + }) + + t.Run("EphemeralNeedsSteps", func(t *testing.T) { + yes := true + _, err := selectedStepsFromBody(nil, nil, nil, nil, nil, &yes) + var apiErr *Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusBadRequest, apiErr.HTTPStatus) + require.Contains(t, apiErr.Message, "ephemeral requires steps") + + no := false + _, err = selectedStepsFromBody(nil, nil, nil, nil, nil, &no) + require.NoError(t, err) + }) + t.Run("OutputsNeedSteps", func(t *testing.T) { - _, err := selectedStepsFromBody(nil, nil, &map[string]map[string]string{"build": {"RESULT": "x"}}) + _, err := selectedStepsFromBody(nil, nil, nil, &map[string]map[string]string{"build": {"RESULT": "x"}}, nil, nil) var apiErr *Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusBadRequest, apiErr.HTTPStatus) require.Contains(t, apiErr.Message, "outputs requires steps") }) } + +// An ephemeral run takes explicit outputs with no source run, is found by its +// ID, appears in no listing, and refuses retry, reschedule and edit. +func TestStartStepsEphemeral(t *testing.T) { + ctx := context.Background() + tmp := t.TempDir() + api, _ := setupEditRetryAPI(t, tmp, editRetrySourceYAML()) + dag, err := spec.LoadYAML(ctx, []byte(editRetryEditedYAMLWithWorkerSelector())) + require.NoError(t, err) + api.dagRunRepository = runtestutil.NewFileDAGRunRepositoryWithEphemeral( + filepath.Join(tmp, "dag-runs"), + filepath.Join(tmp, "dag-runs-ephemeral"), + persis.DAGRunRepositoryOptions{LatestStatusToday: true}, + ) + api.coordinatorCli = &retryCoordinatorRecorder{} + + err = api.startSelectedSteps(ctx, dag, selectedStepsStart{ + selection: stepSelection{ + steps: []string{"consume"}, + outputs: map[string]map[string]string{"build": {"RESULT": "given"}}, + outputsSources: map[string]intake.OutputSource{"build": {RunID: "captured-run"}}, + ephemeral: true, + }, + dagRunID: "trace-free", + }) + require.NoError(t, err) + + attempt, err := api.dagRunRepository.FindAttempt(ctx, ir.NewDAGRunRef(dag.Name, "trace-free")) + require.NoError(t, err) + status, err := attempt.ReadStatus(ctx) + require.NoError(t, err) + require.True(t, status.Ephemeral) + require.Equal(t, []ir.ReusedInput{{Step: "build", RunID: "captured-run", Via: ir.ReusedInputViaCache}}, status.ReusedInputs) + raw, ok := status.Nodes[0].OutputVariables.Load("RESULT") + require.True(t, ok) + require.Equal(t, "RESULT=given", raw) + + listed, err := api.dagRunRepository.ListStatuses(ctx, persis.DAGRunListOptions{AllHistory: true, Unbounded: true}) + require.NoError(t, err) + require.Empty(t, listed) + + var apiErr *Error + require.ErrorAs(t, rejectEphemeralRun(status, "retried"), &apiErr) + require.Equal(t, http.StatusConflict, apiErr.HTTPStatus) + require.NoError(t, rejectEphemeralRun(&ir.DAGRunStatus{}, "retried")) + + _, err = api.retryDAGRun(ctx, dag.Name, "trace-free", "", "", "", false, false) + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusConflict, apiErr.HTTPStatus) +} + +// The audit entry of a start request names the supplied outputs without their +// values and marks an ephemeral run. +func TestSelectedStepsAudit(t *testing.T) { + details := map[string]any{} + addSelectedStepsAudit(details, stepSelection{ + steps: []string{"consume"}, + outputs: map[string]map[string]string{"build": {"RESULT": "secret"}}, + ephemeral: true, + }) + require.Equal(t, true, details["ephemeral"]) + require.Equal(t, []string{"build.RESULT"}, details["outputs"]) + + details = map[string]any{} + addSelectedStepsAudit(details, stepSelection{steps: []string{"consume"}}) + require.NotContains(t, details, "ephemeral") +} diff --git a/internal/service/frontend/api/v1/step_samples.go b/internal/service/frontend/api/v1/step_samples.go new file mode 100644 index 0000000000..0a45dca283 --- /dev/null +++ b/internal/service/frontend/api/v1/step_samples.go @@ -0,0 +1,283 @@ +// Copyright (C) 2026 Yota Hamada +// SPDX-License-Identifier: GPL-3.0-or-later + +package api + +import ( + "context" + "errors" + "fmt" + "math" + "net/http" + "time" + + api "github.com/dagucloud/dagu/v2/api/v1" + "github.com/dagucloud/dagu/v2/internal/ir" + "github.com/dagucloud/dagu/v2/internal/samples" +) + +// The handlers of the samples, events and availability of a continuous step. +// See specs/083-continuous-runs.md. + +const ( + defaultSamplePoints = 600 + minSamplePoints = 10 + maxSamplePoints = 2000 + defaultEventLimit = 100 + maxEventLimit = 500 + defaultSampleWindow = 24 * time.Hour +) + +// attemptDirProvider is implemented by the attempts that live in a directory. +type attemptDirProvider interface { + Dir() string +} + +// openSamplesReader opens the reader of the samples store in dir. Tests replace +// it through the samplesReader field of API. +func (a *API) openSamplesReader(dir string) samples.Reader { + if a.samplesReader != nil { + return a.samplesReader(dir) + } + return samples.OpenReader(dir) +} + +// isContinuousNode reports whether the node is a continuous step: a step that +// repeats without a limit. The runner persists the flag; a run that was +// written without it is read by its repeat policy. +func isContinuousNode(node *ir.Node) bool { + if node.Continuous { + return true + } + rp := node.Step.RepeatPolicy + return rp.RepeatMode != "" && rp.Limit == 0 && rp.LimitStr == "" +} + +// continuousStepReader finds the run attempt and the step, and opens the +// reader of the store of the step. A non-empty notFound is the message of the +// 404 response. +func (a *API) continuousStepReader(ctx context.Context, name, dagRunID, stepName string) (reader samples.Reader, notFound string, err error) { + attempt, notFound, err := a.findVisibleAttempt(ctx, name, dagRunID) + if err != nil || notFound != "" { + return nil, notFound, err + } + status, err := attempt.ReadStatus(ctx) + if err != nil { + return nil, "", fmt.Errorf("error reading dag-run status: %w", err) + } + var node *ir.Node + if status != nil { + node = findStepOutputsNode(status, stepName) + } + if node == nil { + return nil, fmt.Sprintf("step %s not found in dag-run of DAG %s", stepName, name), nil + } + if !isContinuousNode(node) { + return nil, fmt.Sprintf("step %s is not a continuous step", stepName), nil + } + provider, ok := attempt.(attemptDirProvider) + if !ok || provider.Dir() == "" { + return nil, "", fmt.Errorf("attempt of dag-run %s has no directory", dagRunID) + } + return a.openSamplesReader(samples.Dir(provider.Dir(), node.Step.Name)), "", nil +} + +func badRequest(format string, args ...any) *Error { + return &Error{ + HTTPStatus: http.StatusBadRequest, + Code: api.ErrorCodeBadRequest, + Message: fmt.Sprintf(format, args...), + } +} + +func checkIntRange(name string, value *int, lo, hi int) error { + if value != nil && (*value < lo || *value > hi) { + return badRequest("%s must be between %d and %d", name, lo, hi) + } + return nil +} + +func (a *API) GetDAGRunStepSamples(ctx context.Context, request api.GetDAGRunStepSamplesRequestObject) (api.GetDAGRunStepSamplesResponseObject, error) { + p := request.Params + if err := checkIntRange("points", p.Points, minSamplePoints, maxSamplePoints); err != nil { + return nil, err + } + to := time.Now().UTC() + if p.To != nil { + to = p.To.UTC() + } + from := to.Add(-defaultSampleWindow) + if p.From != nil { + from = p.From.UTC() + } + if from.After(to) { + return nil, badRequest("from must not be after to") + } + points := defaultSamplePoints + if p.Points != nil { + points = *p.Points + } + + reader, notFound, err := a.continuousStepReader(ctx, request.Name, request.DagRunId, request.StepName) + if err != nil { + return nil, err + } + if notFound != "" { + return api.GetDAGRunStepSamples404JSONResponse{Code: api.ErrorCodeNotFound, Message: notFound}, nil + } + + series, err := reader.Samples(ctx, samples.SampleQuery{From: from, To: to, MaxPoints: points}) + if err != nil { + return nil, fmt.Errorf("error reading samples: %w", err) + } + return api.GetDAGRunStepSamples200JSONResponse(toStepSamples(series, from, to)), nil +} + +func toStepSamples(series samples.Series, from, to time.Time) api.StepSamples { + if !series.From.IsZero() { + from = series.From + } + if !series.To.IsZero() { + to = series.To + } + buckets := make([]api.SampleBucket, 0, len(series.Buckets)) + for _, b := range series.Buckets { + buckets = append(buckets, api.SampleBucket{ + Time: b.Time.UTC(), + Count: countInt64(b.Count), + Failures: countInt64(b.Failures), + MinMs: b.Min.Milliseconds(), + MaxMs: b.Max.Milliseconds(), + P50Ms: b.P50.Milliseconds(), + P95Ms: b.P95.Milliseconds(), + }) + } + return api.StepSamples{ + From: from.UTC(), + To: to.UTC(), + ResolutionSeconds: int(series.Tier.Width() / time.Second), + Buckets: buckets, + } +} + +func (a *API) GetDAGRunStepEvents(ctx context.Context, request api.GetDAGRunStepEventsRequestObject) (api.GetDAGRunStepEventsResponseObject, error) { + p := request.Params + if err := checkIntRange("limit", p.Limit, 1, maxEventLimit); err != nil { + return nil, err + } + query := samples.EventQuery{Limit: defaultEventLimit} + if p.Limit != nil { + query.Limit = *p.Limit + } + if p.From != nil { + query.From = p.From.UTC() + } + if p.To != nil { + query.To = p.To.UTC() + } + if p.From != nil && p.To != nil && query.From.After(query.To) { + return nil, badRequest("from must not be after to") + } + if p.Before != nil { + query.Before = *p.Before + } + + reader, notFound, err := a.continuousStepReader(ctx, request.Name, request.DagRunId, request.StepName) + if err != nil { + return nil, err + } + if notFound != "" { + return api.GetDAGRunStepEvents404JSONResponse{Code: api.ErrorCodeNotFound, Message: notFound}, nil + } + + page, err := reader.Events(ctx, query) + if errors.Is(err, samples.ErrInvalidCursor) { + return nil, badRequest("invalid cursor") + } + if err != nil { + return nil, fmt.Errorf("error reading events: %w", err) + } + return api.GetDAGRunStepEvents200JSONResponse(toStepEvents(page)), nil +} + +func toStepEvents(page samples.EventPage) api.StepEvents { + events := make([]api.StepEvent, 0, len(page.Events)) + for _, e := range page.Events { + ev := api.StepEvent{Time: e.Time.UTC(), Kind: api.StepEventKind(e.Kind)} + if e.Kind == samples.EventFailed { + ev.ExitCode = ptrOf(e.ExitCode) + } + if e.Message != "" { + ev.Message = ptrOf(e.Message) + } + if e.FailedCount > 0 && (e.Kind == samples.EventFailed || e.Kind == samples.EventRecovered) { + ev.FailedCount = ptrOf(countInt64(e.FailedCount)) + } + if e.Kind == samples.EventRecovered { + ev.OutageSeconds = new(int64(e.Outage / time.Second)) + } + events = append(events, ev) + } + result := api.StepEvents{Events: events} + if page.NextCursor != "" { + result.NextCursor = ptrOf(page.NextCursor) + } + return result +} + +// availabilityWindows lists the windows of the response, in this order. +var availabilityWindows = []struct { + window samples.Window + api api.AvailabilityWindowWindow +}{ + {samples.Window24h, api.AvailabilityWindowWindowN24h}, + {samples.Window7d, api.AvailabilityWindowWindowN7d}, + {samples.Window30d, api.AvailabilityWindowWindowN30d}, + {samples.Window365d, api.AvailabilityWindowWindowN365d}, +} + +func (a *API) GetDAGRunStepAvailability(ctx context.Context, request api.GetDAGRunStepAvailabilityRequestObject) (api.GetDAGRunStepAvailabilityResponseObject, error) { + reader, notFound, err := a.continuousStepReader(ctx, request.Name, request.DagRunId, request.StepName) + if err != nil { + return nil, err + } + if notFound != "" { + return api.GetDAGRunStepAvailability404JSONResponse{Code: api.ErrorCodeNotFound, Message: notFound}, nil + } + + availability, err := reader.Availability(ctx, time.Now().UTC()) + if err != nil { + return nil, fmt.Errorf("error reading availability: %w", err) + } + return api.GetDAGRunStepAvailability200JSONResponse(toStepAvailability(availability)), nil +} + +func toStepAvailability(av samples.Availability) api.StepAvailability { + stats := make(map[samples.Window]samples.WindowStat, len(av.Windows)) + for _, w := range av.Windows { + stats[w.Window] = w + } + windows := make([]api.AvailabilityWindow, 0, len(availabilityWindows)) + for _, w := range availabilityWindows { + stat := stats[w.window] + out := api.AvailabilityWindow{Window: w.api, Count: countInt64(stat.Count), Failures: countInt64(stat.Failures)} + if stat.Count > 0 { + out.Availability = new(float64(stat.Count-min(stat.Failures, stat.Count)) / float64(stat.Count)) + } + windows = append(windows, out) + } + result := api.StepAvailability{ + Windows: windows, + Streak: countInt64(av.Streak), + TotalCount: countInt64(av.TotalCount), + } + if !av.LastFailureAt.IsZero() { + result.LastFailureAt = ptrOf(av.LastFailureAt.UTC()) + } + return result +} + +// countInt64 converts a sample count for the API, saturating at MaxInt64. +func countInt64(n uint64) int64 { + return int64(min(n, math.MaxInt64)) // #nosec G115 -- clamped to MaxInt64 +} diff --git a/internal/service/frontend/api/v1/step_samples_test.go b/internal/service/frontend/api/v1/step_samples_test.go new file mode 100644 index 0000000000..692fada4ad --- /dev/null +++ b/internal/service/frontend/api/v1/step_samples_test.go @@ -0,0 +1,409 @@ +// Copyright (C) 2026 Yota Hamada +// SPDX-License-Identifier: GPL-3.0-or-later + +package api + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + openapiv1 "github.com/dagucloud/dagu/v2/api/v1" + "github.com/dagucloud/dagu/v2/internal/auth" + "github.com/dagucloud/dagu/v2/internal/cmn/config" + "github.com/dagucloud/dagu/v2/internal/ir" + "github.com/dagucloud/dagu/v2/internal/persis" + runtimepkg "github.com/dagucloud/dagu/v2/internal/runtime" + "github.com/dagucloud/dagu/v2/internal/samples" + "github.com/dagucloud/dagu/v2/internal/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// fakeSamplesReader answers from fixed values and records the queries. +type fakeSamplesReader struct { + series samples.Series + page samples.EventPage + availability samples.Availability + + sampleQuery samples.SampleQuery + eventQuery samples.EventQuery + dir string + eventsErr error // returned by Events when set +} + +func (f *fakeSamplesReader) Samples(_ context.Context, q samples.SampleQuery) (samples.Series, error) { + f.sampleQuery = q + return f.series, nil +} + +func (f *fakeSamplesReader) Events(_ context.Context, q samples.EventQuery) (samples.EventPage, error) { + f.eventQuery = q + return f.page, f.eventsErr +} + +func (f *fakeSamplesReader) Availability(_ context.Context, _ time.Time) (samples.Availability, error) { + return f.availability, nil +} + +func continuousNode(name string) *ir.Node { + return &ir.Node{ + Step: ir.Step{Name: name, RepeatPolicy: ir.RepeatPolicy{RepeatMode: ir.RepeatModeWhile, Interval: time.Second}}, + Status: ir.NodeRunning, + } +} + +func newSamplesAPI(t *testing.T, fake *fakeSamplesReader, labels []string, nodes ...*ir.Node) (*API, string) { + t.Helper() + repository := testutil.NewFileDAGRunRepository(t.TempDir(), persis.DAGRunRepositoryOptions{}) + ctx := t.Context() + dag := &ir.DAG{Name: "demo", Labels: ir.NewLabels(labels)} + attempt, err := repository.CreateAttempt(ctx, dag, time.Now().UTC(), "run-1", persis.DAGRunCreateAttemptOptions{}) + require.NoError(t, err) + require.NoError(t, attempt.Open(ctx)) + status := ir.InitialStatus(dag) + status.DAGRunID = "run-1" + status.AttemptID = attempt.ID() + status.Status = ir.Running + status.Nodes = nodes + require.NoError(t, attempt.Write(ctx, status)) + require.NoError(t, attempt.Close(ctx)) + cfg := &config.Config{} + a := &API{ + dagRunRepository: repository, + dagRunMgr: runtimepkg.NewManager(repository, nil, cfg), + config: cfg, + samplesReader: func(dir string) samples.Reader { + fake.dir = dir + return fake + }, + } + return a, attempt.(interface{ Dir() string }).Dir() +} + +func getSamples(t *testing.T, a *API, step string, params openapiv1.GetDAGRunStepSamplesParams) openapiv1.GetDAGRunStepSamplesResponseObject { + t.Helper() + resp, err := a.GetDAGRunStepSamples(t.Context(), openapiv1.GetDAGRunStepSamplesRequestObject{ + Name: "demo", DagRunId: "latest", StepName: step, Params: params, + }) + require.NoError(t, err) + return resp +} + +func requireBadRequest(t *testing.T, err error) { + t.Helper() + var apiErr *Error + require.ErrorAs(t, err, &apiErr) + assert.Equal(t, http.StatusBadRequest, apiErr.HTTPStatus) + assert.Equal(t, openapiv1.ErrorCodeBadRequest, apiErr.Code) +} + +func TestSamplesNotContinuous(t *testing.T) { + limited := continuousNode("limited") + limited.Step.RepeatPolicy.Limit = 5 + plain := &ir.Node{Step: ir.Step{Name: "plain"}} + a, _ := newSamplesAPI(t, &fakeSamplesReader{}, nil, limited, plain, continuousNode("beat")) + ctx := t.Context() + + for _, step := range []string{"limited", "plain"} { + want := "step " + step + " is not a continuous step" + resp, err := a.GetDAGRunStepSamples(ctx, openapiv1.GetDAGRunStepSamplesRequestObject{Name: "demo", DagRunId: "run-1", StepName: step}) + require.NoError(t, err) + nf, ok := resp.(openapiv1.GetDAGRunStepSamples404JSONResponse) + require.True(t, ok, "got %T", resp) + assert.Equal(t, want, nf.Message) + + ev, err := a.GetDAGRunStepEvents(ctx, openapiv1.GetDAGRunStepEventsRequestObject{Name: "demo", DagRunId: "run-1", StepName: step}) + require.NoError(t, err) + nfe, ok := ev.(openapiv1.GetDAGRunStepEvents404JSONResponse) + require.True(t, ok, "got %T", ev) + assert.Equal(t, want, nfe.Message) + + av, err := a.GetDAGRunStepAvailability(ctx, openapiv1.GetDAGRunStepAvailabilityRequestObject{Name: "demo", DagRunId: "run-1", StepName: step}) + require.NoError(t, err) + nfa, ok := av.(openapiv1.GetDAGRunStepAvailability404JSONResponse) + require.True(t, ok, "got %T", av) + assert.Equal(t, want, nfa.Message) + } + + resp := getSamples(t, a, "missing", openapiv1.GetDAGRunStepSamplesParams{}) + _, ok := resp.(openapiv1.GetDAGRunStepSamples404JSONResponse) + assert.True(t, ok, "got %T", resp) +} + +func TestSamplesDefaults(t *testing.T) { + fake := &fakeSamplesReader{} + a, attemptDir := newSamplesAPI(t, fake, nil, continuousNode("beat")) + + before := time.Now().UTC() + resp := getSamples(t, a, "beat", openapiv1.GetDAGRunStepSamplesParams{}) + after := time.Now().UTC() + + _, ok := resp.(openapiv1.GetDAGRunStepSamples200JSONResponse) + require.True(t, ok, "got %T", resp) + assert.Equal(t, samples.Dir(attemptDir, "beat"), fake.dir) + q := fake.sampleQuery + assert.Equal(t, 600, q.MaxPoints) + assert.False(t, q.To.Before(before) || q.To.After(after)) + assert.Equal(t, 24*time.Hour, q.To.Sub(q.From)) +} + +func TestSamplesParams(t *testing.T) { + from := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + to := from.Add(48 * time.Hour) + points := 2000 + + fake := &fakeSamplesReader{} + a, _ := newSamplesAPI(t, fake, nil, continuousNode("beat")) + getSamples(t, a, "beat", openapiv1.GetDAGRunStepSamplesParams{From: &from, To: &to, Points: &points}) + assert.Equal(t, samples.SampleQuery{From: from, To: to, MaxPoints: 2000}, fake.sampleQuery) + + // Only from: to is now. + getSamples(t, a, "beat", openapiv1.GetDAGRunStepSamplesParams{From: &from}) + assert.Equal(t, from, fake.sampleQuery.From) + assert.WithinDuration(t, time.Now(), fake.sampleQuery.To, time.Minute) +} + +func TestSamplesBadParams(t *testing.T) { + a, _ := newSamplesAPI(t, &fakeSamplesReader{}, nil, continuousNode("beat")) + from := time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC) + to := from.Add(-time.Hour) + for name, params := range map[string]openapiv1.GetDAGRunStepSamplesParams{ + "points low": {Points: ptrOf(9)}, + "points high": {Points: ptrOf(2001)}, + "from > to": {From: &from, To: &to}, + } { + t.Run(name, func(t *testing.T) { + _, err := a.GetDAGRunStepSamples(t.Context(), openapiv1.GetDAGRunStepSamplesRequestObject{ + Name: "demo", DagRunId: "run-1", StepName: "beat", Params: params, + }) + requireBadRequest(t, err) + }) + } + for _, n := range []int{10, 2000} { + resp := getSamples(t, a, "beat", openapiv1.GetDAGRunStepSamplesParams{Points: &n}) + _, ok := resp.(openapiv1.GetDAGRunStepSamples200JSONResponse) + assert.True(t, ok, "points %d: got %T", n, resp) + } +} + +func TestSamplesMapping(t *testing.T) { + t0 := time.Date(2026, 3, 1, 12, 0, 0, 0, time.FixedZone("x", 3600)) + fake := &fakeSamplesReader{series: samples.Series{ + Tier: samples.TierHour, + From: t0, To: t0.Add(2 * time.Hour), + Buckets: []samples.Bucket{{ + Time: t0, Count: 3600, Failures: 12, + Min: 1500 * time.Microsecond, Max: 2 * time.Second, P50: 40 * time.Millisecond, P95: 900 * time.Millisecond, + }}, + }} + a, _ := newSamplesAPI(t, fake, nil, continuousNode("beat")) + + got, ok := getSamples(t, a, "beat", openapiv1.GetDAGRunStepSamplesParams{}).(openapiv1.GetDAGRunStepSamples200JSONResponse) + require.True(t, ok) + assert.Equal(t, 3600, got.ResolutionSeconds) + assert.Equal(t, time.UTC, got.From.Location()) + assert.True(t, t0.Equal(got.From)) + assert.True(t, t0.Add(2*time.Hour).Equal(got.To)) + require.Len(t, got.Buckets, 1) + b := got.Buckets[0] + assert.Equal(t, time.UTC, b.Time.Location()) + assert.True(t, t0.Equal(b.Time)) + assert.Equal(t, int64(3600), b.Count) + assert.Equal(t, int64(12), b.Failures) + assert.Equal(t, int64(1), b.MinMs) + assert.Equal(t, int64(2000), b.MaxMs) + assert.Equal(t, int64(40), b.P50Ms) + assert.Equal(t, int64(900), b.P95Ms) +} + +func TestSamplesEmptyStore(t *testing.T) { + a, _ := newSamplesAPI(t, &fakeSamplesReader{}, nil, continuousNode("beat")) + + got, ok := getSamples(t, a, "beat", openapiv1.GetDAGRunStepSamplesParams{}).(openapiv1.GetDAGRunStepSamples200JSONResponse) + require.True(t, ok) + assert.NotNil(t, got.Buckets) + assert.Empty(t, got.Buckets) + assert.False(t, got.From.IsZero()) + assert.Equal(t, 1, got.ResolutionSeconds) + + ev, err := a.GetDAGRunStepEvents(t.Context(), openapiv1.GetDAGRunStepEventsRequestObject{Name: "demo", DagRunId: "run-1", StepName: "beat"}) + require.NoError(t, err) + events, ok := ev.(openapiv1.GetDAGRunStepEvents200JSONResponse) + require.True(t, ok) + assert.NotNil(t, events.Events) + assert.Empty(t, events.Events) + assert.Nil(t, events.NextCursor) + + av, err := a.GetDAGRunStepAvailability(t.Context(), openapiv1.GetDAGRunStepAvailabilityRequestObject{Name: "demo", DagRunId: "run-1", StepName: "beat"}) + require.NoError(t, err) + avail, ok := av.(openapiv1.GetDAGRunStepAvailability200JSONResponse) + require.True(t, ok) + require.Len(t, avail.Windows, 4) + for _, w := range avail.Windows { + assert.Zero(t, w.Count) + assert.Nil(t, w.Availability) + } + assert.Nil(t, avail.LastFailureAt) +} + +func TestEvents(t *testing.T) { + t0 := time.Date(2026, 3, 1, 12, 0, 0, 0, time.UTC) + fake := &fakeSamplesReader{page: samples.EventPage{ + NextCursor: "c2", + Events: []samples.Event{ + {Time: t0.Add(3 * time.Minute), Kind: samples.EventRecovered, FailedCount: 120, Outage: 2 * time.Minute}, + {Time: t0, Kind: samples.EventFailed, ExitCode: 7, Message: "boom"}, + {Time: t0.Add(-time.Hour), Kind: samples.EventStarted}, + // A stop during an outage keeps its count for the next start only. + {Time: t0.Add(-2 * time.Hour), Kind: samples.EventStopped, FailedCount: 3}, + // An outage shorter than a second still has a length. + {Time: t0.Add(-3 * time.Hour), Kind: samples.EventRecovered, FailedCount: 1, Outage: 500 * time.Millisecond}, + }, + }} + a, _ := newSamplesAPI(t, fake, nil, continuousNode("beat")) + ctx := t.Context() + limit, before := 500, "c1" + resp, err := a.GetDAGRunStepEvents(ctx, openapiv1.GetDAGRunStepEventsRequestObject{ + Name: "demo", DagRunId: "run-1", StepName: "beat", + Params: openapiv1.GetDAGRunStepEventsParams{Limit: &limit, Before: &before}, + }) + require.NoError(t, err) + got, ok := resp.(openapiv1.GetDAGRunStepEvents200JSONResponse) + require.True(t, ok) + assert.Equal(t, samples.EventQuery{Limit: 500, Before: "c1"}, fake.eventQuery) + require.NotNil(t, got.NextCursor) + assert.Equal(t, "c2", *got.NextCursor) + require.Len(t, got.Events, 5) + + rec, failed, started := got.Events[0], got.Events[1], got.Events[2] + assert.Equal(t, openapiv1.StepEventKindRecovered, rec.Kind) + require.NotNil(t, rec.OutageSeconds) + assert.Equal(t, int64(120), *rec.OutageSeconds) + require.NotNil(t, rec.FailedCount) + assert.Equal(t, int64(120), *rec.FailedCount) + assert.Nil(t, rec.ExitCode) + + assert.Equal(t, openapiv1.StepEventKindFailed, failed.Kind) + require.NotNil(t, failed.ExitCode) + assert.Equal(t, 7, *failed.ExitCode) + require.NotNil(t, failed.Message) + assert.Equal(t, "boom", *failed.Message) + assert.Nil(t, failed.OutageSeconds) + + assert.Equal(t, openapiv1.StepEventKindStarted, started.Kind) + assert.Nil(t, started.ExitCode) + assert.Nil(t, started.Message) + assert.Nil(t, started.FailedCount) + + assert.Nil(t, got.Events[3].FailedCount) + require.NotNil(t, got.Events[4].OutageSeconds) + assert.Equal(t, int64(0), *got.Events[4].OutageSeconds) +} + +func TestEventsDefaultsAndBadParams(t *testing.T) { + fake := &fakeSamplesReader{} + a, _ := newSamplesAPI(t, fake, nil, continuousNode("beat")) + ctx := t.Context() + call := func(p openapiv1.GetDAGRunStepEventsParams) (openapiv1.GetDAGRunStepEventsResponseObject, error) { + return a.GetDAGRunStepEvents(ctx, openapiv1.GetDAGRunStepEventsRequestObject{ + Name: "demo", DagRunId: "run-1", StepName: "beat", Params: p, + }) + } + + _, err := call(openapiv1.GetDAGRunStepEventsParams{}) + require.NoError(t, err) + assert.Equal(t, 100, fake.eventQuery.Limit) + + for _, n := range []int{0, 501} { + _, err := call(openapiv1.GetDAGRunStepEventsParams{Limit: &n}) + requireBadRequest(t, err) + } + from := time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC) + to := from.Add(-time.Hour) + _, err = call(openapiv1.GetDAGRunStepEventsParams{From: &from, To: &to}) + requireBadRequest(t, err) + + fake.eventsErr = fmt.Errorf("decode: %w", samples.ErrInvalidCursor) + bad := "x" + _, err = call(openapiv1.GetDAGRunStepEventsParams{Before: &bad}) + requireBadRequest(t, err) +} + +func TestAvailability(t *testing.T) { + last := time.Date(2026, 3, 1, 12, 0, 0, 0, time.FixedZone("x", 3600)) + fake := &fakeSamplesReader{availability: samples.Availability{ + // The reader may list the windows in any order, and omit some. + Windows: []samples.WindowStat{ + {Window: samples.Window30d, Count: 1000, Failures: 10}, + {Window: samples.Window24h, Count: 100, Failures: 0}, + {Window: samples.Window7d, Count: 0}, + // Every sample failed: the availability is zero, not absent. + {Window: samples.Window365d, Count: 5, Failures: 5}, + }, + Streak: 42, LastFailureAt: last, TotalCount: 5_000_000_000, + }} + a, _ := newSamplesAPI(t, fake, nil, continuousNode("beat")) + + resp, err := a.GetDAGRunStepAvailability(t.Context(), openapiv1.GetDAGRunStepAvailabilityRequestObject{Name: "demo", DagRunId: "run-1", StepName: "beat"}) + require.NoError(t, err) + got, ok := resp.(openapiv1.GetDAGRunStepAvailability200JSONResponse) + require.True(t, ok) + require.Len(t, got.Windows, 4) + want := []openapiv1.AvailabilityWindowWindow{ + openapiv1.AvailabilityWindowWindowN24h, openapiv1.AvailabilityWindowWindowN7d, + openapiv1.AvailabilityWindowWindowN30d, openapiv1.AvailabilityWindowWindowN365d, + } + for i, w := range got.Windows { + assert.Equal(t, want[i], w.Window) + } + require.NotNil(t, got.Windows[0].Availability) + assert.InDelta(t, 1.0, *got.Windows[0].Availability, 1e-9) + assert.Nil(t, got.Windows[1].Availability) + require.NotNil(t, got.Windows[2].Availability) + assert.InDelta(t, 0.99, *got.Windows[2].Availability, 1e-9) + assert.Equal(t, int64(1000), got.Windows[2].Count) + assert.Equal(t, int64(10), got.Windows[2].Failures) + require.NotNil(t, got.Windows[3].Availability) + assert.Zero(t, *got.Windows[3].Availability) + assert.Equal(t, int64(42), got.Streak) + assert.Equal(t, int64(5_000_000_000), got.TotalCount) + require.NotNil(t, got.LastFailureAt) + assert.Equal(t, time.UTC, got.LastFailureAt.Location()) + assert.True(t, last.Equal(*got.LastFailureAt)) +} + +// The handlers follow the visibility rule of the other step handlers: a user +// with no access to the workspace of the run gets 404. +func TestSamplesWorkspaceVisibility(t *testing.T) { + a, _ := newSamplesAPI(t, &fakeSamplesReader{}, []string{"workspace=ops"}, continuousNode("beat")) + a.authService = struct{ AuthService }{} + user := func(workspace string) *auth.User { + return &auth.User{Role: auth.RoleViewer, WorkspaceAccess: &auth.WorkspaceAccess{ + Grants: []auth.WorkspaceGrant{{Workspace: workspace, Role: auth.RoleViewer}}, + }} + } + + _, err := a.GetDAGRunStepSamples(auth.WithUser(t.Context(), user("secret")), openapiv1.GetDAGRunStepSamplesRequestObject{ + Name: "demo", DagRunId: "run-1", StepName: "beat", + }) + var apiErr *Error + require.ErrorAs(t, err, &apiErr) + assert.Equal(t, http.StatusNotFound, apiErr.HTTPStatus) + + resp, err := a.GetDAGRunStepSamples(auth.WithUser(t.Context(), user("ops")), openapiv1.GetDAGRunStepSamplesRequestObject{ + Name: "demo", DagRunId: "run-1", StepName: "beat", + }) + require.NoError(t, err) + _, ok := resp.(openapiv1.GetDAGRunStepSamples200JSONResponse) + assert.True(t, ok, "got %T", resp) +} + +func TestSamplesStoreDir(t *testing.T) { + fake := &fakeSamplesReader{} + a, attemptDir := newSamplesAPI(t, fake, nil, continuousNode("my beat")) + getSamples(t, a, "my beat", openapiv1.GetDAGRunStepSamplesParams{}) + assert.Equal(t, samples.Dir(attemptDir, "my beat"), fake.dir) +} diff --git a/internal/service/frontend/api/v1/transformer.go b/internal/service/frontend/api/v1/transformer.go index 2134a29ef8..dfb9185e23 100644 --- a/internal/service/frontend/api/v1/transformer.go +++ b/internal/service/frontend/api/v1/transformer.go @@ -371,6 +371,7 @@ func toDAGRunSummary(s ir.DAGRunStatus) api.DAGRunSummary { FinishedAt: s.FinishedAt, ArtifactsAvailable: artifactsAvailable, NoReuse: ptrOf(s.NoReuse), + Ephemeral: ephemeralFlag(s), Status: api.Status(s.Status), StatusLabel: api.StatusLabel(s.Status.String()), WorkerId: ptrOf(s.WorkerID), @@ -382,6 +383,36 @@ func toDAGRunSummary(s ir.DAGRunStatus) api.DAGRunSummary { } } +// ephemeralFlag reports the ephemeral flag of a run, absent for a normal run. +func ephemeralFlag(s ir.DAGRunStatus) *bool { + if !s.Ephemeral { + return nil + } + return ptrOf(true) +} + +// toReusedInputs converts the provenance of the carried outputs of a run. +func toReusedInputs(inputs []ir.ReusedInput) *[]api.ReusedInput { + if len(inputs) == 0 { + return nil + } + out := make([]api.ReusedInput, 0, len(inputs)) + for _, input := range inputs { + item := api.ReusedInput{Step: input.Step, RunId: input.RunID, Via: api.ReusedInputVia(input.Via)} + if input.CapturedAt != "" { + item.CapturedAt = ptrOf(input.CapturedAt) + } + if input.StartedAt != "" { + item.StartedAt = ptrOf(input.StartedAt) + } + if input.FinishedAt != "" { + item.FinishedAt = ptrOf(input.FinishedAt) + } + out = append(out, item) + } + return &out +} + func toDAGRunsPageResponse(page persis.DAGRunStatusPage) api.DAGRunsPageResponse { dagRuns := make([]api.DAGRunSummary, 0, len(page.Items)) for _, item := range page.Items { @@ -440,6 +471,8 @@ func ToDAGRunDetails(s ir.DAGRunStatus) api.DAGRunDetails { ParentDAGRunId: ptrOf(s.Parent.ID), ArtifactsAvailable: artifactsAvailable, NoReuse: ptrOf(s.NoReuse), + Ephemeral: ephemeralFlag(s), + ReusedInputs: toReusedInputs(s.ReusedInputs), Log: s.Log, Name: s.Name, Params: ptrOf(s.Params), @@ -543,6 +576,14 @@ func hasArtifactEntries(archiveDir string) bool { return false } +// continuousFlag gives the API value of Node.continuous: set only when true. +func continuousFlag(continuous bool) *bool { + if !continuous { + return nil + } + return ptrOf(true) +} + func toNode(node *ir.Node) api.Node { if node == nil { return api.Node{} @@ -568,6 +609,7 @@ func toNode(node *ir.Node) api.Node { Error: ptrOf(node.Error), SubRuns: ptrOf(toSubDAGRuns(node.SubRuns)), SubRunsRepeated: ptrOf(toSubDAGRuns(node.SubRunsRepeated)), + Continuous: continuousFlag(isContinuousNode(node)), HumanTaskCompletedBy: ptrOf(node.HumanTaskCompletedBy), HumanTaskCompletedById: ptrOf(node.HumanTaskCompletedByID), ApprovedAt: ptrOf(node.ApprovedAt), diff --git a/internal/service/frontend/ephemeral_cleanup.go b/internal/service/frontend/ephemeral_cleanup.go new file mode 100644 index 0000000000..fbf389a4a7 --- /dev/null +++ b/internal/service/frontend/ephemeral_cleanup.go @@ -0,0 +1,73 @@ +// Copyright (C) 2026 Yota Hamada +// SPDX-License-Identifier: GPL-3.0-or-later + +package frontend + +import ( + "context" + "time" + + "github.com/dagucloud/dagu/v2/internal/cmn/logger" + "github.com/dagucloud/dagu/v2/internal/cmn/logger/tag" + "github.com/dagucloud/dagu/v2/internal/persis" +) + +// ephemeralCleanupInterval is the gap between two passes that remove ended +// ephemeral runs. +const ephemeralCleanupInterval = time.Minute + +// ephemeralCleanupConfig tunes the removal of ephemeral runs. A zero field +// selects the default. +type ephemeralCleanupConfig struct { + ttl time.Duration + interval time.Duration +} + +// WithEphemeralRunCleanup sets how long an ephemeral run stays after it ends +// and how often the server looks for runs to remove. It exists for tests; the +// defaults are persis.EphemeralRunTTL and one minute. +func WithEphemeralRunCleanup(ttl, interval time.Duration) ServerOption { + return func(s *Server) { + s.ephemeralCleanup = ephemeralCleanupConfig{ttl: ttl, interval: interval} + } +} + +// startEphemeralRunCleanup removes the ephemeral runs that an earlier process +// left, then keeps removing each one once it has ended for the TTL. A run that +// has not ended is never removed. +func (srv *Server) startEphemeralRunCleanup(ctx context.Context) { + repo := srv.dagRunRepository + if repo == nil { + return + } + ttl := srv.ephemeralCleanup.ttl + if ttl <= 0 { + ttl = persis.EphemeralRunTTL + } + interval := srv.ephemeralCleanup.interval + if interval <= 0 { + interval = ephemeralCleanupInterval + } + + // A run left by an earlier process has no owner that reads it any more. + if removed, err := repo.RemoveEndedEphemeralRuns(ctx, 0); err != nil { + logger.Warn(ctx, "Failed to remove ephemeral runs left at start", tag.Error(err)) + } else if removed > 0 { + logger.Info(ctx, "Removed ephemeral runs left at start", tag.Count(removed)) + } + + go func() { + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + if _, err := repo.RemoveEndedEphemeralRuns(ctx, ttl); err != nil { + logger.Warn(ctx, "Failed to remove ended ephemeral runs", tag.Error(err)) + } + } + } + }() +} diff --git a/internal/service/frontend/server.go b/internal/service/frontend/server.go index 6237cbdf5a..50624303de 100644 --- a/internal/service/frontend/server.go +++ b/internal/service/frontend/server.go @@ -136,6 +136,8 @@ type Server struct { tunnelAPIOpts []apiv1.APIOption tunnelService *tunnel.Service dagRepository *persis.DAGRepository + dagRunRepository *persis.DAGRunRepository + ephemeralCleanup ephemeralCleanupConfig licenseManager *license.Manager remoteNodeResolver *remotenode.Resolver upgradeStore upgrade.CacheStore @@ -628,6 +630,7 @@ func NewServer(setup ServerConfig, opts ...ServerOption) (*Server, error) { syncService: syncSvc, metricsRegistry: mr, dagRepository: dr, + dagRunRepository: dagRunRepository, remoteNodeResolver: remoteNodeResolver, upgradeStore: upgradeStore, workspaceDeprecation: workspaceDeprecation, @@ -1009,6 +1012,7 @@ func (srv *Server) Serve(ctx context.Context) error { metrics.StartUptime(ctx) logger.Info(ctx, "Server is starting", tag.Addr(srv.listener.Addr().String()), tag.URL(webURL)) srv.startPeriodicUpdateCheck(ctx) + srv.startEphemeralRunCleanup(ctx) serveErr := make(chan error, 1) go srv.startServer(ctx, serveErr) diff --git a/llms.txt b/llms.txt index 33f18165ef..5c764b7fc7 100644 --- a/llms.txt +++ b/llms.txt @@ -1846,8 +1846,9 @@ Flags: - `--run-id/-r` — Custom run ID - `--from-run-id` — Historic dag-run ID to use as the template for a new run - `--only` — Run only this step (name or ID) in a new run of the current definition; repeatable. Other steps are recorded as skipped. Not with `--from-run-id` -- `--outputs-from` — Finished run of the same DAG whose step outputs and work directory feed the `--only` steps (requires `--only`) -- `--output` — Output of a step skipped by `--only`, as `.=`; repeatable, takes precedence over `--outputs-from` (requires `--only`) +- `--outputs-from` — Finished run of the same DAG whose work directory, and the outputs of the skipped steps that the `--only` steps depend on, feed the run (requires `--only`) +- `--output-from` — Run that gives the outputs of ONE skipped step, as `=`; repeatable, takes precedence over `--outputs-from` for that step (requires `--only`). The run must be a finished run of the same DAG, and the step must have succeeded there +- `--output` — Deprecated: use `--output-from`. Output of a step skipped by `--only`, as `.=`; repeatable, takes precedence over `--outputs-from` and `--output-from` (requires `--only`) - `--labels` — Additional labels (comma-separated key=value or key-only) - `--tags` — Deprecated alias for `--labels` - `--default-working-dir` — Default working directory for DAGs without explicit workingDir diff --git a/skills/dagu/references/cli.md b/skills/dagu/references/cli.md index 86859bc446..dd836407a7 100644 --- a/skills/dagu/references/cli.md +++ b/skills/dagu/references/cli.md @@ -21,8 +21,9 @@ Flags: - `--run-id/-r` — Custom run ID - `--from-run-id` — Historic dag-run ID to use as the template for a new run - `--only` — Run only this step (name or ID) in a new run of the current definition; repeatable. Other steps are recorded as skipped. Not with `--from-run-id` -- `--outputs-from` — Finished run of the same DAG whose step outputs and work directory feed the `--only` steps (requires `--only`) -- `--output` — Output of a step skipped by `--only`, as `.=`; repeatable, takes precedence over `--outputs-from` (requires `--only`) +- `--outputs-from` — Finished run of the same DAG whose work directory, and the outputs of the skipped steps that the `--only` steps depend on, feed the run (requires `--only`) +- `--output-from` — Run that gives the outputs of ONE skipped step, as `=`; repeatable, takes precedence over `--outputs-from` for that step (requires `--only`). The run must be a finished run of the same DAG, and the step must have succeeded there +- `--output` — Deprecated: use `--output-from`. Output of a step skipped by `--only`, as `.=`; repeatable, takes precedence over `--outputs-from` and `--output-from` (requires `--only`) - `--labels` — Additional labels (comma-separated key=value or key-only) - `--tags` — Deprecated alias for `--labels` - `--default-working-dir` — Default working directory for DAGs without explicit workingDir diff --git a/specs/013-step-run.md b/specs/013-step-run.md index 334be618a7..d75f5dbcdb 100644 --- a/specs/013-step-run.md +++ b/specs/013-step-run.md @@ -310,7 +310,7 @@ Shell selection order is: - `outputsSources` names the run that each step of `outputs` was captured from. It is an object keyed by step name or ID. Each value has `runId` and an optional `capturedAt`. It requires `steps`. -- Every step in `outputs` MUST have an entry in `outputsSources`. A step in `outputs` with no entry is rejected with HTTP 400 and the message `outputs for step X must name the run they come from (outputsSources)`. An entry for a step that is not in `outputs` is also rejected with HTTP 400. +- `outputsSources` is optional. A step in `outputs` with no entry is accepted and has no entry in `ReusedInputs`. An entry for a step that is not in `outputs` is rejected with HTTP 400. - `runId` MUST be a valid run ID, as for `outputsFromRunId`. The run does not need to exist, because the values can come from a run that was removed. `capturedAt` is an RFC 3339 timestamp. An invalid `runId` or `capturedAt` is rejected with HTTP 400. @@ -334,7 +334,7 @@ Shell selection order is: - A step named in `--output-from` takes its outputs from its own run, not from `--outputs-from`. The run records `ReusedInputs` with `via` set to `run`, as for the server. -- The command has no flag that sets an output value. The flag `--output .=` was removed. The command stops with the message `the --output flag was removed: use --output-from =`. +- `--output .=` is the command line form of `outputs`. It is deprecated in favor of `--output-from`, and it still works: it requires `--only`, can be repeated, and takes precedence over `--outputs-from` and `--output-from` for that output. The command has no form of `outputsSources`, so a step set by `--output` has no entry in `ReusedInputs`. #### Handlers of a Selected-Steps Run @@ -356,7 +356,7 @@ A graph end step is a step that no other step depends on. A dependency is an ent A selected-steps run records where the carried outputs of its skipped steps come from. The record is `ReusedInputs` in the run status. -- `ReusedInputs` has one entry for each skipped step that carried outputs. The order of the entries is the order of the steps in the DAG. +- `ReusedInputs` has one entry for each skipped step that carried outputs, except a step in `outputs` with no entry in `outputsSources`. The order of the entries is the order of the steps in the DAG. - Only a skipped step that a selected step depends on, directly or through other steps, carries outputs. This includes a step that `outputs` or `outputsFromRuns` names. A skipped step that no selected step depends on, such as a step after the selected steps, carries no outputs, no log and no times. It is recorded as skipped, has no entry, and shows no mark of reuse. @@ -368,7 +368,7 @@ A selected-steps run records where the carried outputs of its skipped steps come - `via` is `cache` for a step whose outputs come from `outputs`. Then `runId` and `capturedAt` come from `outputsSources`, and the entry has no `startedAt` and no `finishedAt`. The web UI does not use this form. -- A step that has a run and supplied outputs has the `cache` entry, because the values in `outputs` take precedence. +- A step that has a run and supplied outputs has the `cache` entry, because the values in `outputs` take precedence. Without an entry in `outputsSources`, it has no entry. - A step that carried no outputs has no entry. diff --git a/specs/021-mcp-read-tool.md b/specs/021-mcp-read-tool.md index 47a2c109bd..e0c4bb04de 100644 --- a/specs/021-mcp-read-tool.md +++ b/specs/021-mcp-read-tool.md @@ -158,6 +158,8 @@ Minimum `data` models: | Wiki page | `data.id` is the page path string. `data.content` is the Markdown string. `data.mimeType` is `text/markdown`. `data.uri` is the canonical `dagu://wiki/{workspace}/{path}` URI. | | Wiki search | `data.results` is an array. Each result has `id` string, `uri` string, `matches` array of snippets, and `hasMoreMatches` boolean. `data.hasMore` is a boolean, and `data.nextCursor` is an opaque cursor string when another page is available. | +Every `startedAt` and `finishedAt` value in these models is an RFC 3339 string with millisecond precision, for example `2026-10-27T08:02:02.158Z`. A run that an older version stored can have second precision, with no fraction. + Implementations may add fields inside `data`. Conformance tests must not require absence of additional fields. diff --git a/specs/083-continuous-runs.md b/specs/083-continuous-runs.md index f55565b26a..16eb0b57f7 100644 --- a/specs/083-continuous-runs.md +++ b/specs/083-continuous-runs.md @@ -6,6 +6,11 @@ Partially implemented. This spec defines the behavior of a step that repeats without end, such as a heartbeat that checks a service every second for years. +Covered: which steps are continuous, the samples and events of a step run by +the local runner, the tiers and their retention, the three API queries and +their errors, the status writes and the compaction of the status file, and the +bounded step log. `conformance/spec083_continuous_runs` checks this behavior. + Not covered yet: runs that a distributed worker executes, the status writes of a continuous sub-DAG step, a retry of a run (it starts a new store), and the time range of the step log. @@ -263,6 +268,32 @@ one element for each sample. - An API request of this spec answers in 500 ms at most for a run of three years at one sample each second, on the reference machine of the tests. +## Errors + +- A query parameter out of its range, `from` after `to`, or a `before` cursor + that the server did not give gets `400`. +- A request for a run or a step that does not exist, or for a step that is not + continuous, gets `404`. +- A store that cannot open does not stop the step: the step keeps running and + records no sample. + +## Examples + +A step that repeats until a file exists is a continuous step: + +```yaml +steps: + - id: beat + run: sh beat.sh + repeat_policy: + repeat: until + condition: test -f done + interval_sec: 0 +``` + +The same step with `limit` is a repeated step, not a continuous one. The +fixtures in `conformance/spec083_continuous_runs/testdata` give both. + ## Test Scenarios - A step with `limit` records no sample and keeps the status behavior of today. diff --git a/ui/src/api/v1/schema.ts b/ui/src/api/v1/schema.ts index 2470c5b7a0..b403c8a8b6 100644 --- a/ui/src/api/v1/schema.ts +++ b/ui/src/api/v1/schema.ts @@ -1197,6 +1197,86 @@ export interface paths { patch?: never; trace?: never; }; + "/dag-runs/{name}/{dagRunId}/steps/{stepName}/samples": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Retrieve the samples of a continuous step, in buckets + * @description Returns the executions of a continuous step as buckets of one tier (1 second, 1 minute, 1 hour or 24 hours). The server picks the finest tier whose retention covers `from` and whose bucket count in the window is not more than `points`. The response has at most `points` buckets and does not depend on the count of executions. + */ + get: operations["getDAGRunStepSamples"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/dag-runs/{name}/{dagRunId}/steps/{stepName}/events": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Retrieve the events of a continuous step + * @description Lists the events of a continuous step, the newest first: start, stop, failed and recovered. + */ + get: operations["getDAGRunStepEvents"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/dag-runs/{name}/{dagRunId}/steps/{stepName}/availability": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Retrieve the availability of a continuous step + * @description Gives the availability of the last 24 hours, 7 days, 30 days and 365 days, and the current streak. + */ + get: operations["getDAGRunStepAvailability"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/dag-runs/{name}/{dagRunId}/steps/{stepName}/outputs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Retrieve outputs of a specific step in a DAG-run + * @description Fetches the persisted outputs and the output variable of one step in a DAG-run. The stepName parameter accepts a step name or a step id. The runtime masks secret values before it persists them. Each value is cut at 65536 bytes. + */ + get: operations["getDAGRunStepOutputs"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/dag-runs/{name}/{dagRunId}/steps/{stepName}/log/download": { parameters: { query?: never; @@ -5898,6 +5978,94 @@ export interface components { */ class?: ValueReferenceNoticeClass; }; + /** @description One structured validation result of a DAG spec. */ + ValidationIssue: { + /** + * @description Severity of the issue. + * @enum {string} + */ + severity: ValidationIssueSeverity; + /** + * @description Stage that produced the issue. + * @enum {string} + */ + source: ValidationIssueSource; + /** @description Human-readable message. For errors, the same text as the matching errors entry. */ + message: string; + /** @description Normalized name of the step, when known. */ + stepName?: string; + /** @description Step key such as steps[3] or handler_on.failure, when known. */ + stepPath?: string; + /** @description DAG field path such as steps[3].with.url or consts.image, when known. */ + fieldPath?: string; + /** @description 1-based line in the spec, when known. */ + line?: number; + /** @description 1-based column in the spec, when known. */ + column?: number; + /** @description Machine-readable reason, for value-reference notices. */ + reason?: string; + }; + /** @description Values that a step of the DAG can reference. */ + ReferenceScope: { + params: components["schemas"]["ReferenceScopeVariable"][]; + consts: components["schemas"]["ReferenceScopeVariable"][]; + /** @description Environment variable names. Values are not included. */ + env: components["schemas"]["ReferenceScopeVariable"][]; + /** @description Secret names. Values are not included. */ + secrets: components["schemas"]["ReferenceScopeVariable"][]; + steps: components["schemas"]["ReferenceScopeStep"][]; + }; + /** @description A named value that a step can reference. */ + ReferenceScopeVariable: { + name: string; + /** @description Canonical reference token, such as ${params.region}. */ + reference: string; + description?: string; + /** @description Default value. Set only for params and consts. */ + defaultValue?: string; + }; + /** @description A step and the outputs that other steps can reference. */ + ReferenceScopeStep: { + name: string; + id?: string; + /** @description Step key such as steps[3] or handler_on.exit. */ + stepPath: string; + /** @description Names of all ancestor steps. */ + upstream: string[]; + outputs: components["schemas"]["ReferenceScopeOutput"][]; + /** + * @description static when the outputs are known from the spec, runtime when only a run gives them. + * @enum {string} + */ + outputsKnown: ReferenceScopeStepOutputsKnown; + }; + /** @description An output of a step that other steps can reference. */ + ReferenceScopeOutput: { + name: string; + /** @description Reference token, such as ${steps.fetch.outputs.body} or ${RESULT} for an output variable. */ + reference: string; + /** @enum {string} */ + source: ReferenceScopeOutputSource; + type?: string; + }; + /** @description Persisted outputs of one step in a DAG-run. */ + StepOutputs: { + stepName: string; + stepId?: string; + status: components["schemas"]["NodeStatus"]; + statusLabel: components["schemas"]["NodeStatusLabel"]; + outputs: components["schemas"]["StepOutputValue"][]; + outputVariable?: components["schemas"]["StepOutputValue"]; + }; + /** @description One output value of a step. The value is cut at 65536 bytes. */ + StepOutputValue: { + name: string; + value: string; + /** @description True when the value was cut. */ + truncated: boolean; + /** @description Size of the value in bytes before the cut. */ + size: number; + }; /** @description Editor-only metadata used to synthesize per-document schema hints */ DAGEditorHints: { /** @description Deprecated legacy execution definitions inherited from base config and available to the current DAG */ @@ -6051,14 +6219,16 @@ export interface components { autoRetryLimit?: number | null; /** @description RFC 3339 timestamp of when the DAG-run was scheduled to run */ scheduleTime?: string; - /** @description RFC 3339 timestamp when the DAG-run started */ + /** @description RFC 3339 timestamp with millisecond precision when the DAG-run started. A run that an older version stored has second precision. */ startedAt: string; - /** @description RFC 3339 timestamp when the DAG-run finished */ + /** @description RFC 3339 timestamp with millisecond precision when the DAG-run finished. A run that an older version stored has second precision. */ finishedAt: string; /** @description Whether artifact files are available for this DAG-run */ artifactsAvailable: boolean; /** @description Whether reuse of prior build materializations was disabled for this DAG-run */ noReuse?: boolean; + /** @description True for an ephemeral run (a test run that leaves no trace). Absent or false for other runs. Only the DAG history with includeEphemeral and the run by ID return such a run. */ + ephemeral?: boolean; /** @description Runtime parameters passed to the DAG-run in JSON format */ params?: string; /** @description Runtime profile selected for this DAG-run. */ @@ -6079,6 +6249,29 @@ export interface components { */ tags?: string[]; }; + /** @description The run that a step's supplied outputs were captured from. */ + ReusedOutputSource: { + runId: components["schemas"]["DAGRunId"]; + /** @description RFC 3339 timestamp of when the outputs were captured. Optional. */ + capturedAt?: string; + }; + /** @description Provenance of the outputs that a skipped step of a selected-steps run carries. */ + ReusedInput: { + /** @description Name of the skipped step */ + step: string; + runId: components["schemas"]["DAGRunId"]; + /** @description RFC 3339 timestamp of when the outputs were captured. Absent when unknown. */ + capturedAt?: string; + /** @description RFC 3339 timestamp with millisecond precision of when the step started in the source run. Present for via run; absent for via cache. */ + startedAt?: string; + /** @description RFC 3339 timestamp with millisecond precision of when the step finished in the source run. Present for via run; absent for via cache. */ + finishedAt?: string; + /** + * @description run: the outputs were carried from the run named by outputsFromRunId, or by outputsFromRuns for this step. cache: a client that is not the web UI supplied the outputs and named the run they were captured from in outputsSources. + * @enum {string} + */ + via: ReusedInputVia; + }; /** @description Detailed status of a DAG-run including sub DAG-run nodes */ DAGRunDetails: components["schemas"]["DAGRunSummary"] & { /** @description Name of the root DAG-run */ @@ -6116,6 +6309,8 @@ export interface components { approvalResumePending?: boolean; /** @description Identifier of the attempt this status belongs to. Each retry of the DAG-run, including a resume after manual action, records a new attempt. */ attemptId?: string; + /** @description Where the carried outputs of the skipped steps of a selected-steps run come from, one entry per skipped step, in DAG step order. Absent when the run carried no outputs. */ + reusedInputs?: components["schemas"]["ReusedInput"][]; }; /** @description One file within a DAG-run's artifact directory */ ArtifactListFile: { @@ -6243,9 +6438,9 @@ export interface components { attempt?: number; /** @description Agent's justification, or why the call was rejected */ reason?: string; - /** @description RFC3339 timestamp when the step started */ + /** @description RFC 3339 timestamp with millisecond precision when the step started */ startedAt?: string; - /** @description RFC3339 timestamp when the step finished */ + /** @description RFC 3339 timestamp with millisecond precision when the step finished */ finishedAt?: string; /** @description Child DAG-run this action produced, for linking to its run page. Absent for steps that run no child DAG. */ childDagRunId?: string; @@ -6293,9 +6488,9 @@ export interface components { stdout: string; /** @description Path to the standard error log file for this step */ stderr: string; - /** @description RFC3339 timestamp when the step started */ + /** @description RFC 3339 timestamp with millisecond precision when the step started. A step that an older version stored has second precision. */ startedAt: string; - /** @description RFC3339 timestamp when the step finished */ + /** @description RFC 3339 timestamp with millisecond precision when the step finished. A step that an older version stored has second precision. */ finishedAt: string; status: components["schemas"]["NodeStatus"]; statusLabel: components["schemas"]["NodeStatusLabel"]; @@ -6303,6 +6498,8 @@ export interface components { retryCount: number; /** @description Number of successful completions for repeating steps */ doneCount: number; + /** @description True for a continuous step: a step that repeats without a limit. Its executions are recorded as samples (see the samples, events and availability endpoints). */ + continuous?: boolean; /** @description List of sub DAG-runs associated with this step */ subRuns?: components["schemas"]["SubDAGRun"][]; /** @description List of repeated sub DAG-runs when using repeatPolicy */ @@ -6522,9 +6719,9 @@ export interface components { dagName?: string; status: components["schemas"]["Status"]; statusLabel: components["schemas"]["StatusLabel"]; - /** @description RFC 3339 timestamp when the sub DAG-run started */ + /** @description RFC 3339 timestamp with millisecond precision when the sub DAG-run started */ startedAt: string; - /** @description RFC 3339 timestamp when the sub DAG-run finished */ + /** @description RFC 3339 timestamp with millisecond precision when the sub DAG-run finished */ finishedAt?: string; }; /** @description One named value output or build file output */ @@ -6872,9 +7069,9 @@ export interface components { statusLabel: components["schemas"]["NodeStatusLabel"]; /** @description Error the body run reported, when it failed */ error?: string; - /** @description RFC3339 timestamp when the body run started */ + /** @description RFC 3339 timestamp with millisecond precision when the body run started */ startedAt?: string; - /** @description RFC3339 timestamp when the body run finished */ + /** @description RFC 3339 timestamp with millisecond precision when the body run finished */ finishedAt?: string; }; /** @description One item of a foreach step with the status of each body step */ @@ -6886,7 +7083,9 @@ export interface components { status: components["schemas"]["NodeStatus"]; statusLabel: components["schemas"]["NodeStatusLabel"]; error?: string; + /** @description RFC 3339 timestamp with millisecond precision when the body run started */ startedAt?: string; + /** @description RFC 3339 timestamp with millisecond precision when the body run finished */ finishedAt?: string; /** @description Body steps in execution order */ steps: components["schemas"]["ForeachBodyStep"][]; @@ -6898,7 +7097,9 @@ export interface components { status: components["schemas"]["NodeStatus"]; statusLabel: components["schemas"]["NodeStatusLabel"]; error?: string; + /** @description RFC 3339 timestamp with millisecond precision when the body step started */ startedAt?: string; + /** @description RFC 3339 timestamp with millisecond precision when the body step finished */ finishedAt?: string; retryCount?: number; /** @description Whether a stdout log file was recorded for the step */ @@ -6910,6 +7111,98 @@ export interface components { agentSession?: components["schemas"]["AgentSession"]; modelUsage?: components["schemas"]["StepModelUsage"]; }; + /** @description The samples of one time bucket of a continuous step */ + SampleBucket: { + /** + * Format: date-time + * @description Start of the bucket, in UTC + */ + time: string; + /** + * Format: int64 + * @description Count of samples in the bucket + */ + count: number; + /** + * Format: int64 + * @description Count of failed samples (exit code not 0) + */ + failures: number; + /** Format: int64 */ + minMs: number; + /** Format: int64 */ + maxMs: number; + /** Format: int64 */ + p50Ms: number; + /** Format: int64 */ + p95Ms: number; + }; + StepSamples: { + /** @description Width of the tier that the buckets come from: 1, 60, 3600 or 86400 */ + resolutionSeconds: number; + /** Format: date-time */ + from: string; + /** Format: date-time */ + to: string; + /** @description Buckets in time order. A bucket with no sample is not listed. */ + buckets: components["schemas"]["SampleBucket"][]; + }; + /** @enum {string} */ + StepEventKind: StepEventKind; + StepEvent: { + /** Format: date-time */ + time: string; + kind: components["schemas"]["StepEventKind"]; + exitCode?: number; + /** @description Last line of the standard error of a failed execution, 256 bytes at most */ + message?: string; + /** + * Format: int64 + * @description For `recovered` and for a repeated `failed` event: count of failed samples of the outage + */ + failedCount?: number; + /** + * Format: int64 + * @description For `recovered`: length of the outage + */ + outageSeconds?: number; + }; + StepEvents: { + events: components["schemas"]["StepEvent"][]; + /** @description Cursor for the next page. Absent when there are no more events. */ + nextCursor?: string; + }; + AvailabilityWindow: { + /** @enum {string} */ + window: AvailabilityWindowWindow; + /** Format: int64 */ + count: number; + /** Format: int64 */ + failures: number; + /** + * Format: double + * @description From 0 to 1. Absent when `count` is 0. + */ + availability?: number; + }; + StepAvailability: { + windows: components["schemas"]["AvailabilityWindow"][]; + /** + * Format: int64 + * @description Count of samples since the last failed sample. A lower bound once the raw tier no longer holds that sample. + */ + streak: number; + /** + * Format: date-time + * @description Time of the last failed sample, or the start of its aggregate bucket once the raw tier no longer holds it. Absent when none failed. + */ + lastFailureAt?: string; + /** + * Format: int64 + * @description Count of all executions + */ + totalCount: number; + }; /** @description Log information for the execution */ Log: { /** @description Log content */ @@ -9300,6 +9593,11 @@ export interface operations { errors: string[]; /** @description Non-fatal spec warnings; these do not affect valid */ warnings?: string[]; + /** @description Structured errors, warnings and notices of the spec, with step and location data when known */ + issues: components["schemas"]["ValidationIssue"][]; + /** @description Passive value-reference notices produced while loading this spec. These notices are not persisted. */ + valueReferenceNotices: components["schemas"]["ValueReferenceNotice"][]; + referenceScope?: components["schemas"]["ReferenceScope"]; }; }; }; @@ -9446,12 +9744,25 @@ export interface operations { /** @description Run only these steps, by step name or ID, in a new DAG-run of the current definition. Every other step is recorded as skipped and does not run. Not supported for agent DAGs. */ steps?: string[]; outputsFromRunId?: components["schemas"]["DAGRunId"] & unknown; - /** @description Outputs of skipped steps, keyed by step name or ID and then by output name. A declared output, or any name when the step declares none, resolves through ${steps..outputs.}; the name of the step's string-form output variable sets that variable. Takes precedence over outputs from outputsFromRunId. Requires steps. */ + /** @description The run that each skipped step carries its outputs from, keyed by step name or ID. A step named here takes its outputs from that run in place of outputsFromRunId; the work directory still comes only from outputsFromRunId. Each run must exist for the same DAG (a normal or an ephemeral run), be finished, and hold a node of the step that succeeded or that carries copied outputs; otherwise the request returns 400 with a message that names the step and the run. A step selected to run cannot be a key. The web UI names the run of each reused step with this field and never sends values. Requires steps. */ + outputsFromRuns?: { + [key: string]: components["schemas"]["DAGRunId"]; + }; + /** @description Outputs of skipped steps, keyed by step name or ID and then by output name. For clients that are not the web UI; the web UI sends no values. A declared output, or any name when the step declares none, resolves through ${steps..outputs.}; the name of the step's string-form output variable sets that variable. Takes precedence over outputs from outputsFromRunId and outputsFromRuns, and works without them: the caller can supply every value itself, and can name the run the values of each step come from in outputsSources. A reference to an output that nobody supplied stays unresolved text in the step. Requires steps. */ outputs?: { [key: string]: { [key: string]: string; }; }; + /** @description Optional. The run each step of outputs was captured from, keyed by step name or ID. Dagu records it in the reusedInputs of the new run; a step of outputs without an entry has no entry there. An entry for a step not in outputs returns 400. The run is not required to exist any more. Requires steps. */ + outputsSources?: { + [key: string]: components["schemas"]["ReusedOutputSource"]; + }; + /** + * @description If true, the run leaves no trace. It runs like any run and is readable by its ID (details, step logs, step outputs, spec, SSE) while it exists, but it is not listed anywhere by default (run lists, DAG history unless includeEphemeral is set, last run, queues, dashboard, artifacts, events and notifications), it does not count in retention, and it cannot be retried, rescheduled or edited (409). The server removes it 10 minutes after it ends and at server start. Allowed only together with steps; a request for the whole DAG with ephemeral true returns 400. The audit log keeps the request, marked ephemeral. + * @default false + */ + ephemeral?: boolean; /** * @deprecated * @description Deprecated alias for `labels`; mutually exclusive with `labels`. @@ -9689,6 +10000,8 @@ export interface operations { query?: { /** @description name of the remote node */ remoteNode?: components["parameters"]["RemoteNode"]; + /** @description If true, the response also contains the ephemeral runs of this DAG, merged by start time with the other runs under the same limit. Each carries ephemeral true. No other list returns them. */ + includeEphemeral?: boolean; }; header?: never; path: { @@ -9788,6 +10101,8 @@ export interface operations { dag?: components["schemas"]["DAGDetails"]; /** @description The DAG spec in YAML format */ spec: string; + /** @description Lowercase hex SHA-256 of the file bytes that spec holds */ + sha256?: string; /** @description List of errors in the spec */ errors: string[]; /** @description Non-fatal spec warnings */ @@ -9826,6 +10141,8 @@ export interface operations { "application/json": { /** @description The new DAG spec in YAML format */ spec: string; + /** @description Lowercase hex SHA-256 of the file that the client edited. When set and different from the current file, the server writes nothing and returns 409. */ + expectedSha256?: string; }; }; }; @@ -9839,9 +10156,20 @@ export interface operations { "application/json": { /** @description List of errors in the spec */ errors: string[]; + /** @description Lowercase hex SHA-256 of the bytes written */ + sha256: string; }; }; }; + /** @description The current file does not match expectedSha256 */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; /** @description Generic error response */ default: { headers: { @@ -10606,12 +10934,25 @@ export interface operations { /** @description Run only these steps, by step name or ID, in a new DAG-run of the current definition. Every other step is recorded as skipped and does not run. Not supported for agent DAGs. */ steps?: string[]; outputsFromRunId?: components["schemas"]["DAGRunId"] & unknown; - /** @description Outputs of skipped steps, keyed by step name or ID and then by output name. A declared output, or any name when the step declares none, resolves through ${steps..outputs.}; the name of the step's string-form output variable sets that variable. Takes precedence over outputs from outputsFromRunId. Requires steps. */ + /** @description The run that each skipped step carries its outputs from, keyed by step name or ID. A step named here takes its outputs from that run in place of outputsFromRunId; the work directory still comes only from outputsFromRunId. Each run must exist for the same DAG (a normal or an ephemeral run), be finished, and hold a node of the step that succeeded or that carries copied outputs; otherwise the request returns 400 with a message that names the step and the run. A step selected to run cannot be a key. The web UI names the run of each reused step with this field and never sends values. Requires steps. */ + outputsFromRuns?: { + [key: string]: components["schemas"]["DAGRunId"]; + }; + /** @description Outputs of skipped steps, keyed by step name or ID and then by output name. For clients that are not the web UI; the web UI sends no values. A declared output, or any name when the step declares none, resolves through ${steps..outputs.}; the name of the step's string-form output variable sets that variable. Takes precedence over outputs from outputsFromRunId and outputsFromRuns, and works without them: the caller can supply every value itself, and can name the run the values of each step come from in outputsSources. A reference to an output that nobody supplied stays unresolved text in the step. Requires steps. */ outputs?: { [key: string]: { [key: string]: string; }; }; + /** @description Optional. The run each step of outputs was captured from, keyed by step name or ID. Dagu records it in the reusedInputs of the new run; a step of outputs without an entry has no entry there. An entry for a step not in outputs returns 400. The run is not required to exist any more. Requires steps. */ + outputsSources?: { + [key: string]: components["schemas"]["ReusedOutputSource"]; + }; + /** + * @description If true, the run leaves no trace. It runs like any run and is readable by its ID (details, step logs, step outputs, spec, SSE) while it exists, but it is not listed anywhere by default (run lists, DAG history unless includeEphemeral is set, last run, queues, dashboard, artifacts, events and notifications), it does not count in retention, and it cannot be retried, rescheduled or edited (409). The server removes it 10 minutes after it ends and at server start. Allowed only together with steps; a request for the whole DAG with ephemeral true returns 400. The audit log keeps the request, marked ephemeral. + * @default false + */ + ephemeral?: boolean; /** * @deprecated * @description Deprecated alias for `labels`; mutually exclusive with `labels`. @@ -11013,7 +11354,7 @@ export interface operations { "application/json": components["schemas"]["Error"]; }; }; - /** @description Conflict (run ID already exists) */ + /** @description Conflict (run ID already exists, or the historic run is ephemeral) */ 409: { headers: { [name: string]: unknown; @@ -11086,6 +11427,15 @@ export interface operations { }; }; }; + /** @description Conflict: the DAG-run is ephemeral */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; /** @description Generic error response */ default: { headers: { @@ -11141,6 +11491,15 @@ export interface operations { }; }; }; + /** @description Conflict: the DAG-run is ephemeral */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; /** @description Generic error response */ default: { headers: { @@ -11577,6 +11936,15 @@ export interface operations { }; content?: never; }; + /** @description Conflict: the DAG-run is ephemeral */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; /** @description Generic error response */ default: { headers: { @@ -11925,6 +12293,230 @@ export interface operations { }; }; }; + getDAGRunStepSamples: { + parameters: { + query?: { + /** @description name of the remote node */ + remoteNode?: components["parameters"]["RemoteNode"]; + /** @description Start of the window (RFC 3339). Default: 24 hours before `to`. */ + from?: string; + /** @description End of the window (RFC 3339). Default: now. */ + to?: string; + /** @description Most buckets to return. */ + points?: number; + }; + header?: never; + path: { + /** @description name of the DAG */ + name: components["parameters"]["DAGName"]; + /** @description ID of the DAG-run or 'latest' to get the most recent DAG-run */ + dagRunId: components["parameters"]["DAGRunId"]; + /** @description name of the step */ + stepName: components["parameters"]["StepName"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description A successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["StepSamples"]; + }; + }; + /** @description A query parameter is not valid or out of its range, or from is after to */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description DAG-run or step not found, or the step is not a continuous step */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Generic error response */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + getDAGRunStepEvents: { + parameters: { + query?: { + /** @description name of the remote node */ + remoteNode?: components["parameters"]["RemoteNode"]; + /** @description Start of the window (RFC 3339), inclusive. Default: no bound. */ + from?: string; + /** @description End of the window (RFC 3339), exclusive. Default: no bound. */ + to?: string; + /** @description Most events to return. */ + limit?: number; + /** @description Opaque cursor from `nextCursor` of a previous response. */ + before?: string; + }; + header?: never; + path: { + /** @description name of the DAG */ + name: components["parameters"]["DAGName"]; + /** @description ID of the DAG-run or 'latest' to get the most recent DAG-run */ + dagRunId: components["parameters"]["DAGRunId"]; + /** @description name of the step */ + stepName: components["parameters"]["StepName"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description A successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["StepEvents"]; + }; + }; + /** @description A query parameter is not valid or out of its range, from is after to, or before is not a cursor that the server gave */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description DAG-run or step not found, or the step is not a continuous step */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Generic error response */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + getDAGRunStepAvailability: { + parameters: { + query?: { + /** @description name of the remote node */ + remoteNode?: components["parameters"]["RemoteNode"]; + }; + header?: never; + path: { + /** @description name of the DAG */ + name: components["parameters"]["DAGName"]; + /** @description ID of the DAG-run or 'latest' to get the most recent DAG-run */ + dagRunId: components["parameters"]["DAGRunId"]; + /** @description name of the step */ + stepName: components["parameters"]["StepName"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description A successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["StepAvailability"]; + }; + }; + /** @description DAG-run or step not found, or the step is not a continuous step */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Generic error response */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + getDAGRunStepOutputs: { + parameters: { + query?: { + /** @description name of the remote node */ + remoteNode?: components["parameters"]["RemoteNode"]; + }; + header?: never; + path: { + /** @description name of the DAG */ + name: components["parameters"]["DAGName"]; + /** @description ID of the DAG-run or 'latest' to get the most recent DAG-run */ + dagRunId: components["parameters"]["DAGRunId"]; + /** @description name of the step */ + stepName: components["parameters"]["StepName"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description A successful response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["StepOutputs"]; + }; + }; + /** @description DAG-run or step not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Generic error response */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; downloadDAGRunStepLog: { parameters: { query?: { @@ -22645,6 +23237,27 @@ export enum ValueReferenceNoticeClass { defect = "defect", runtime_only = "runtime_only" } +export enum ValidationIssueSeverity { + error = "error", + warning = "warning", + notice = "notice" +} +export enum ValidationIssueSource { + load = "load", + build = "build", + warning = "warning", + valueReference = "valueReference" +} +export enum ReferenceScopeStepOutputsKnown { + static = "static", + runtime = "runtime" +} +export enum ReferenceScopeOutputSource { + declared = "declared", + stdoutField = "stdoutField", + outputVariable = "outputVariable", + action = "action" +} export enum ParamDefType { string = "string", integer = "integer", @@ -22656,6 +23269,10 @@ export enum DAGRunConditionStatus { False = "False", Unknown = "Unknown" } +export enum ReusedInputVia { + run = "run", + cache = "cache" +} export enum ArtifactNodeType { directory = "directory", file = "file" @@ -22728,6 +23345,18 @@ export enum ForeachItemStatusFilter { failed = "failed", aborted = "aborted" } +export enum StepEventKind { + started = "started", + stopped = "stopped", + failed = "failed", + recovered = "recovered" +} +export enum AvailabilityWindowWindow { + Value24h = "24h", + Value7d = "7d", + Value30d = "30d", + Value365d = "365d" +} export enum RepeatMode { While = "while", Until = "until" From a61cb7fdda0ed4d0f44a3257ba94ac5df5527740 Mon Sep 17 00:00:00 2001 From: SCcagg5 Date: Wed, 28 Oct 2026 12:39:10 +0100 Subject: [PATCH 2/2] Yaml fixtures of the document model tests --- .../__tests__/fixtures/anchors-aliases.yaml | 25 ++++++++ .../doc/__tests__/fixtures/block-scalars.yaml | 30 +++++++++ .../__tests__/fixtures/browser-flow-maps.yaml | 32 ++++++++++ .../__tests__/fixtures/build-comments.yaml | 61 +++++++++++++++++++ .../doc/__tests__/fixtures/chain-router.yaml | 10 +++ .../fixtures/comments-everywhere.yaml | 28 +++++++++ .../doc/__tests__/fixtures/config-cli.yaml | 8 +++ .../fixtures/demo-agent-release.yaml | 57 +++++++++++++++++ .../__tests__/fixtures/demo-graph-etl.yaml | 52 ++++++++++++++++ .../fixtures/embedded-custom-executor.yaml | 7 +++ .../__tests__/fixtures/embedded-local.yaml | 11 ++++ .../doc/__tests__/fixtures/empty-file.yaml | 0 .../__tests__/fixtures/flow-collections.yaml | 23 +++++++ .../__tests__/fixtures/flow-maps-unicode.yaml | 18 ++++++ .../__tests__/fixtures/folded-line-break.yaml | 8 +++ .../fixtures/foreach-string-items.yaml | 21 +++++++ .../doc/__tests__/fixtures/handler-env.yaml | 13 ++++ .../__tests__/fixtures/handlers-chain.yaml | 20 ++++++ .../doc/__tests__/fixtures/harness-acp.yaml | 13 ++++ .../doc/__tests__/fixtures/invalid-yaml.yaml | 4 ++ .../doc/__tests__/fixtures/long-lines.yaml | 25 ++++++++ .../fixtures/map-steps-forbidden.yaml | 3 + .../doc/__tests__/fixtures/map-steps.yaml | 20 ++++++ .../fixtures/multidoc-comments-markers.yaml | 20 ++++++ .../fixtures/multidoc-empty-between.yaml | 9 +++ .../__tests__/fixtures/multidoc-subdag.yaml | 14 +++++ .../fixtures/multidoc-unnamed-later.yaml | 7 +++ .../__tests__/fixtures/nested-array-run.yaml | 6 ++ .../fixtures/nested-parallel-groups.yaml | 18 ++++++ .../__tests__/fixtures/odd-indentation.yaml | 9 +++ .../fixtures/quoted-keys-and-values.yaml | 17 ++++++ .../fixtures/router-numeric-band.yaml | 11 ++++ .../__tests__/fixtures/router-regex-keys.yaml | 13 ++++ .../doc/__tests__/fixtures/run-context.yaml | 38 ++++++++++++ .../__tests__/fixtures/scheduler-cron.yaml | 5 ++ .../fixtures/script-utf8-multiline.yaml | 9 +++ .../__tests__/fixtures/shorthand-steps.yaml | 9 +++ .../fixtures/spec-harness-comments.yaml | 16 +++++ .../fixtures/spec-outputs-heredoc.yaml | 15 +++++ .../fixtures/spec-subdag-parallel.yaml | 17 ++++++ .../__tests__/fixtures/spec-xlsx-convert.yaml | 9 +++ .../__tests__/fixtures/unicode-comments.yaml | 10 +++ .../doc/__tests__/fixtures/xlsx-japanese.yaml | 42 +++++++++++++ 43 files changed, 783 insertions(+) create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/anchors-aliases.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/block-scalars.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/browser-flow-maps.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/build-comments.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/chain-router.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/comments-everywhere.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/config-cli.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/demo-agent-release.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/demo-graph-etl.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/embedded-custom-executor.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/embedded-local.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/empty-file.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/flow-collections.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/flow-maps-unicode.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/folded-line-break.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/foreach-string-items.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/handler-env.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/handlers-chain.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/harness-acp.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/invalid-yaml.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/long-lines.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/map-steps-forbidden.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/map-steps.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/multidoc-comments-markers.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/multidoc-empty-between.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/multidoc-subdag.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/multidoc-unnamed-later.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/nested-array-run.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/nested-parallel-groups.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/odd-indentation.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/quoted-keys-and-values.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/router-numeric-band.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/router-regex-keys.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/run-context.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/scheduler-cron.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/script-utf8-multiline.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/shorthand-steps.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/spec-harness-comments.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/spec-outputs-heredoc.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/spec-subdag-parallel.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/spec-xlsx-convert.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/unicode-comments.yaml create mode 100644 ui/src/features/visual-editor/doc/__tests__/fixtures/xlsx-japanese.yaml diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/anchors-aliases.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/anchors-aliases.yaml new file mode 100644 index 0000000000..9af713b0fb --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/anchors-aliases.yaml @@ -0,0 +1,25 @@ +# Shared defaults through anchors and aliases. +name: anchors +env: + - COMMON: &common_env + RETRIES: 3 + REGION: eu-west-1 +defaults: &defaults + timeout_sec: 30 + retry_policy: &retry + limit: 2 + interval_sec: 5 +steps: + - name: first + run: echo first + <<: *defaults + - name: second + depends: [first] + run: echo second + retry_policy: *retry # same policy as above + env: *common_env + - name: third + depends: + - second + run: echo third + <<: *defaults diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/block-scalars.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/block-scalars.yaml new file mode 100644 index 0000000000..4e274323dd --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/block-scalars.yaml @@ -0,0 +1,30 @@ +name: block-scalars +description: >- + A folded scalar that + strips the final line break. +steps: + - name: literal + run: | + echo "line one" + echo "indented line" + + echo "after a blank line" + - name: keep + run: |+ + keeps trailing blank lines + + - name: strip + run: |- + no trailing line break + - name: folded + run: > + echo this folded text + is one line + + and a second line + - name: explicit indent + run: |2 + two extra spaces + base + - name: last + run: echo done diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/browser-flow-maps.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/browser-flow-maps.yaml new file mode 100644 index 0000000000..c6519c30f3 --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/browser-flow-maps.yaml @@ -0,0 +1,32 @@ +working_dir: . +env: + - SHOP_URL: ${SHOP_URL} + - LLM_BASE_URL: ${LLM_BASE_URL} +llm: + provider: local + model: test-model + base_url: ${LLM_BASE_URL} +steps: + - id: checkout + action: browser.run + with: + url: ${SHOP_URL} + browser: + screenshots: final + do: + - act: Click the Place order button + when: The page shows 'Cart has 2 items' + - expect: {text: "Order confirmed"} + - extract: + instruction: The order number + schema: + type: object + properties: + order_number: + type: string + - screenshot: confirmation + - id: record + depends: checkout + run: | + echo "${steps.checkout.outputs.order_number}" > order.out + ls "$DAG_RUN_ARTIFACTS_DIR/browser/checkout" > shots.out diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/build-comments.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/build-comments.yaml new file mode 100644 index 0000000000..1bd29ce969 --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/build-comments.yaml @@ -0,0 +1,61 @@ +# Copyright (C) 2026 Yota Hamada +# SPDX-License-Identifier: GPL-3.0-or-later + +type: build +working_dir: . +shell: [sh] + +steps: + - id: build + name: build + inputs: + - name: source + path: source.txt + outputs: + - name: intermediate + path: intermediate.txt + preconditions: + - condition: test -f "${inputs.source}" + script: | + count=$(cat build-count.txt 2>/dev/null || printf 0) + printf '%s\n' "$((count + 1))" > build-count.txt + if [ -f fail.txt ]; then + printf 'broken\n' > "${outputs.intermediate}" + exit 1 + fi + cat "${inputs.source}" > "${outputs.intermediate}" + + - id: consume + name: consume + inputs: + - name: intermediate + path: intermediate.txt + outputs: + - name: result + path: result.txt + env: + INPUT_PATH: ${inputs.intermediate} + OUTPUT_PATH: ${outputs.result} + script: | + count=$(cat consume-count.txt 2>/dev/null || printf 0) + printf '%s\n' "$((count + 1))" > consume-count.txt + cat "$INPUT_PATH" > "$OUTPUT_PATH" + + - id: controlled + name: controlled + depends: build + inputs: + - name: source + path: source.txt + outputs: + - name: result + path: controlled.txt + script: | + count=$(cat controlled-count.txt 2>/dev/null || printf 0) + printf '%s\n' "$((count + 1))" > controlled-count.txt + cat "${inputs.source}" > "${outputs.result}" + + - name: always + script: | + count=$(cat always-count.txt 2>/dev/null || printf 0) + printf '%s\n' "$((count + 1))" > always-count.txt diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/chain-router.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/chain-router.yaml new file mode 100644 index 0000000000..b823eaf196 --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/chain-router.yaml @@ -0,0 +1,10 @@ +type: chain +steps: + - name: pick + action: router.route + with: + value: "a" + routes: + a: [t] + - name: t + run: echo hi diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/comments-everywhere.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/comments-everywhere.yaml new file mode 100644 index 0000000000..4ec4e912b8 --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/comments-everywhere.yaml @@ -0,0 +1,28 @@ +#!/usr/bin/env dagu +# Leading comment + +# Second block of comments +name: comments # trailing on name + +# before params +params: + # inside params + - NAME: world # trailing on item + # after the item + +steps: + # before step one + - name: one # trailing one + # inside step one, before run + run: echo one # trailing run + + # inside step one, after a blank line + # between steps (less indented) + - name: two + depends: [one] # trailing depends + run: echo two + # last comment of step two +# comment at column 0 inside steps + - name: three + run: echo three +# final comment diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/config-cli.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/config-cli.yaml new file mode 100644 index 0000000000..317010f68a --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/config-cli.yaml @@ -0,0 +1,8 @@ +port: 54321 +headless: true +dags: /tmp/.dagu_test +scheduler: + port: 0 +coordinator: + host: 127.0.0.1 + port: 9876 diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/demo-agent-release.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/demo-agent-release.yaml new file mode 100644 index 0000000000..1c6b08dcbc --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/demo-agent-release.yaml @@ -0,0 +1,57 @@ +# Agent: the agent chooses which step to run until every task is done. +type: agent +description: Ship a small release. The agent plans, builds, checks, asks for approval and publishes. +llm: + provider: anthropic + model: claude-opus-5 + max_tool_iterations: 30 + system: | + You ship a small release. Plan first, then build, then check the result. + Ask a person to approve before you publish. Never publish without approval. +params: + - VERSION: 1.4.0 +steps: + - id: plan + name: plan + description: Write the plan of the release. + run: echo "plan for ${params.VERSION}" + + - id: build + name: build + description: Build the release artifacts. + run: echo "building ${params.VERSION}" + + - id: check + name: check + description: Check that the artifacts exist and report their size. + run: echo "artifacts 3, size 12 MB" + + - id: approve + name: approve + description: Ask a person to approve the release before it goes out. + action: human.task + with: + prompt: Approve the release ${params.VERSION}? + form: + type: object + properties: + approved: + type: boolean + note: + type: string + required: [approved] + + - id: publish + name: publish + description: Publish the release. Run it only after the approval. + run: echo "published ${params.VERSION}" + +tasks: + - name: planned + description: Finished when the plan of the release exists. + - name: built + description: Finished when the build ran and the check passed. + - name: approved + description: Finished when a person approved the release. + - name: published + description: Finished when the release was published after the approval. diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/demo-graph-etl.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/demo-graph-etl.yaml new file mode 100644 index 0000000000..577112e820 --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/demo-graph-etl.yaml @@ -0,0 +1,52 @@ +# Graph: two extractions run in parallel, then merge, validate and load. +description: Extract orders and users in parallel, merge them, validate the result and load it. + +schedule: + - 0 9 * * * + - 0 9 * * 1-5 + +params: + - TARGET: staging +type: graph + +steps: + - id: extract_orders + name: extract orders + run: echo '{"orders":42}' + output: + orders: + from: stdout + decode: json + + - id: extract_users + name: extract users + run: echo '{"users":7}' + output: + users: + from: stdout + decode: json + + - id: merge + name: merge + run: echo "merging ${steps.extract_orders.outputs.orders} and ${steps.extract_users.outputs.users}" + depends: [extract_orders, extract_users] + + - id: merge_3 + name: merge_3 + run: echo "merging ${steps.extract_orders.outputs.orders} and ${steps.extract_users.outputs.users}" + depends: [extract_orders, extract_users] + + - id: merge_2 + name: merge_2 + run: echo "merging ${steps.extract_orders.outputs.orders} and ${steps.extract_users.outputs.users}" + depends: [extract_orders, extract_users] + + - id: validate + name: validate + run: echo "validating for ${params.TARGET}" + depends: [merge] + + - id: load + name: load + run: echo "loaded into ${params.TARGET}" + depends: [validate] diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/embedded-custom-executor.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/embedded-custom-executor.yaml new file mode 100644 index 0000000000..8c3779ea4b --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/embedded-custom-executor.yaml @@ -0,0 +1,7 @@ +name: embedded-custom-executor-example +type: graph +steps: + - name: call-go-code + action: embedded_echo + with: + message: domain operation from DAG YAML diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/embedded-local.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/embedded-local.yaml new file mode 100644 index 0000000000..b2e4dda3a2 --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/embedded-local.yaml @@ -0,0 +1,11 @@ +name: embedded-local-example +type: graph +params: + - TARGET +steps: + - name: greet + # Dagu resolves ${TARGET} from run parameters before the shell sees it. + run: echo "hello ${TARGET}" + - name: finish + run: echo "local embedded run complete" + depends: [greet] diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/empty-file.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/empty-file.yaml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/flow-collections.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/flow-collections.yaml new file mode 100644 index 0000000000..49f6adbe69 --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/flow-collections.yaml @@ -0,0 +1,23 @@ +name: flow-collections +params: [ALPHA, BETA] +labels: {team: ops, tier: {name: gold, rank: 1}} +steps: + - {name: inline, run: "echo inline", depends: []} + - name: spaced + depends: [ inline ] + with: { method: GET, url: "https://example.com/?a=1&b=2" } + - name: long flow + run: echo long + env: [FIRST=1, SECOND=2, THIRD=3, FOURTH=4, FIFTH=5, SIXTH=6, SEVENTH=7, EIGHTH=8, NINTH=9, TENTH=10] + - name: multiline flow + run: echo multi + with: + items: [ + one, + two, # second + three + ] + options: { + a: 1, + b: 2 + } diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/flow-maps-unicode.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/flow-maps-unicode.yaml new file mode 100644 index 0000000000..7584c20fe1 --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/flow-maps-unicode.yaml @@ -0,0 +1,18 @@ +working_dir: . +steps: + - id: run + action: computer.run + with: + ai: never + idle: "0" + do: + - launch: + command: powershell.exe + args: [-NoProfile, -ExecutionPolicy, Bypass, -File, elements.ps1] + - wait: 3s + - act: + instruction: Save the form + recorded: + windows: + - click: {app: powershell, window: "Dagu elements test", role: button, name: 保存, id: saveButton} + - expect: {text: 保存しました, within: 10s} diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/folded-line-break.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/folded-line-break.yaml new file mode 100644 index 0000000000..4e9c9140dc --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/folded-line-break.yaml @@ -0,0 +1,8 @@ +working_dir: . +env: + - COMMAND_TEXT: |- + printf 'line-break-ran\n' > line-break-ran.txt + printf 'second\n' >> line-break-ran.txt +steps: + - id: line_break + run: ${env.COMMAND_TEXT} diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/foreach-string-items.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/foreach-string-items.yaml new file mode 100644 index 0000000000..17c07f2856 --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/foreach-string-items.yaml @@ -0,0 +1,21 @@ +params: + - name: episodes + default: '["alpha","beta"]' +working_dir: . +steps: + - name: each + foreach: + items: ${params.episodes} + max_concurrent: 2 + steps: + - id: summarize + run: | + printf '%s\n' '${foreach.item}' + output: BODY_RESULT + collect: + markdown: ${summarize.output} + output: FOREACH_RESULTS + - name: consume + depends: each + run: | + printf '%s\n' "$FOREACH_RESULTS" > foreach-string-results.txt diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/handler-env.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/handler-env.yaml new file mode 100644 index 0000000000..df99f088da --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/handler-env.yaml @@ -0,0 +1,13 @@ +params: + - name: environment + description: Runtime environment. +working_dir: . +handler_on: + success: + env: + HANDLER_VALUE: "${params.environment}" + run: | + printf '%s\n' "$HANDLER_VALUE" > handler-env.txt +steps: + - name: ok + run: "true" diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/handlers-chain.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/handlers-chain.yaml new file mode 100644 index 0000000000..2531d2727c --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/handlers-chain.yaml @@ -0,0 +1,20 @@ +type: chain +name: release +handler_on: + init: + run: echo init + success: + run: echo ok + failure: + # Notify on failure. + run: ./notify.sh + abort: + run: echo aborted + exit: + run: echo exit +steps: + - name: build + run: make build + - name: test + run: make test + - make deploy diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/harness-acp.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/harness-acp.yaml new file mode 100644 index 0000000000..06436f162b --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/harness-acp.yaml @@ -0,0 +1,13 @@ +# Install and authenticate an ACP adapter, such as @agentclientprotocol/codex-acp. +harnesses: + coding-agent: + protocol: acp + binary: codex-acp + +steps: + - id: review + action: harness.run + with: + provider: coding-agent + prompt: Review the code in this directory and summarize its modules without modifying files. + output: REVIEW diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/invalid-yaml.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/invalid-yaml.yaml new file mode 100644 index 0000000000..0b5bf88ddb --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/invalid-yaml.yaml @@ -0,0 +1,4 @@ +steps: + - name: should-not-run + run: echo executed > executed.txt + bad: [ diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/long-lines.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/long-lines.yaml new file mode 100644 index 0000000000..16d853a29e --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/long-lines.yaml @@ -0,0 +1,25 @@ +# A messy workflow with very long lines. A long comment is never reflowed: alpha0 beta1 gamma2 delta3 epsilon4 zeta5 eta6 theta7 iota8 kappa9 lambda10 omega11 alpha12 beta13 gamma14 delta15 epsilon16 zeta17 eta18 theta19 iota20 kappa21 lambda22 omega23 alpha24 beta25 gamma26 delta27 epsilon28 zeta29 +steps: + - name: fetch + description: alpha0 beta1 gamma2 delta3 epsilon4 zeta5 eta6 theta7 iota8 kappa9 lambda10 omega11 alpha12 beta13 gamma14 delta15 epsilon16 zeta17 eta18 theta19 iota20 kappa21 lambda22 omega23 alpha24 beta25 gamma26 delta27 epsilon28 zeta29 + run: curl --fail --silent --show-error --location --header Accept=json --output /tmp/out.json https://example.com/api/v1/items?page=1 + output: ITEMS + - name: report + run: echo "items from ${steps.fetch.outputs.ITEMS} and ${params.TARGET} for the release of ${env.VERSION} on the host ${env.HOSTNAME} in ten words" + depends: [fetch, a_long_name_one, a_long_name_two, a_long_name_three, a_long_name_four, a_long_name_five] + with: {first: 1, second: two, third: three three three three, fourth: four four four, fifth: five five five five five} + - name: quoted + description: "Quoted text with \"inner quotes\" and a tab\t and unicode éè 你好 😀 alpha0 beta1 gamma2 delta3 epsilon4 zeta5 eta6 theta7 iota8 kappa9 lambda10 omega11 alpha12 beta13 gamma14 delta15 epsilon16 zeta17 eta18 theta19" + run: 'echo it''s quoted and long alpha0 beta1 gamma2 delta3 epsilon4 zeta5 eta6 theta7 iota8 kappa9 lambda10 omega11 alpha12 beta13 gamma14 delta15 epsilon16 zeta17 eta18 theta19' + - name: url + description: https://example.com/very-long-path-segment/very-long-path-segment/very-long-path-segment/very-long-path-segment/very-long-path-segment/very-long-path-segment/very-long-path-segment/very-long-path-segment/end + run: echo xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + - name: script + script: | + echo one + echo alpha0 beta1 gamma2 delta3 epsilon4 zeta5 eta6 theta7 iota8 kappa9 lambda10 omega11 alpha12 beta13 gamma14 delta15 epsilon16 zeta17 eta18 theta19 iota20 kappa21 lambda22 omega23 alpha24 + shell: bash +name: messy-long +description: alpha0 beta1 gamma2 delta3 epsilon4 zeta5 eta6 theta7 iota8 kappa9 lambda10 omega11 alpha12 beta13 gamma14 delta15 epsilon16 zeta17 eta18 theta19 iota20 kappa21 lambda22 omega23 alpha24 beta25 gamma26 delta27 epsilon28 zeta29 eta30 theta31 iota32 kappa33 lambda34 omega35 alpha36 beta37 gamma38 delta39 +labels: [one, two] +params: [TARGET=tttttttttttttttttttttttttttttt, VERSION=1.0, P0=value0, P1=value1, P2=value2, P3=value3, P4=value4, P5=value5, P6=value6, P7=value7] diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/map-steps-forbidden.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/map-steps-forbidden.yaml new file mode 100644 index 0000000000..770dbf2d4d --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/map-steps-forbidden.yaml @@ -0,0 +1,3 @@ +steps: + build: + run: echo executed > executed.txt diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/map-steps.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/map-steps.yaml new file mode 100644 index 0000000000..8c897a6e0c --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/map-steps.yaml @@ -0,0 +1,20 @@ +# Map-form steps: the key is the step name. +name: map-form +steps: + # Entry point. + fetch: + run: curl -sSf https://example.com/data.csv -o data.csv # download + + parse: + depends: [fetch] + run: wc -l data.csv + env: + - LC_ALL: C + + "publish-all": + depends: + - parse + action: http.request + with: + method: POST + url: https://example.com/publish diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/multidoc-comments-markers.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/multidoc-comments-markers.yaml new file mode 100644 index 0000000000..9bf4703912 --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/multidoc-comments-markers.yaml @@ -0,0 +1,20 @@ +# First document +name: parent +steps: + - name: call + action: dag.run + with: + dag: child-a + +# comment before the marker +--- # first child +name: child-a +steps: + - name: work + run: echo a +... +--- +name: child-b # last document +steps: + - name: work + run: echo b diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/multidoc-empty-between.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/multidoc-empty-between.yaml new file mode 100644 index 0000000000..25ef01d432 --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/multidoc-empty-between.yaml @@ -0,0 +1,9 @@ +steps: + - name: first + run: echo executed > executed.txt +--- +--- +name: child +steps: + - name: second + run: echo executed > executed.txt diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/multidoc-subdag.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/multidoc-subdag.yaml new file mode 100644 index 0000000000..1e6f7aaf5d --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/multidoc-subdag.yaml @@ -0,0 +1,14 @@ +working_dir: . +steps: + - id: call_child + action: dag.run + with: + dag: depth_child + +--- + +name: depth_child +working_dir: . +steps: + - id: work + run: printf 'child ran\n' > child.out diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/multidoc-unnamed-later.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/multidoc-unnamed-later.yaml new file mode 100644 index 0000000000..8bac309687 --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/multidoc-unnamed-later.yaml @@ -0,0 +1,7 @@ +steps: + - name: first + run: echo executed > executed.txt +--- +steps: + - name: second + run: echo executed > executed.txt diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/nested-array-run.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/nested-array-run.yaml new file mode 100644 index 0000000000..273767cd20 --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/nested-array-run.yaml @@ -0,0 +1,6 @@ +working_dir: . +steps: + - id: bad + run: + - echo one + - [echo, two] diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/nested-parallel-groups.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/nested-parallel-groups.yaml new file mode 100644 index 0000000000..ba39931a4b --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/nested-parallel-groups.yaml @@ -0,0 +1,18 @@ +name: ci # parallel groups are nested arrays +steps: + - name: prepare + run: ./prepare.sh + + # Group 1: quality gates. + - - name: lint + run: make lint + - name: unit + run: make test + # trailing comment inside the group item + - echo shorthand inside the group + + # Group 2 has one item only. + - - name: package + run: make package + - name: report + run: ./report.sh diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/odd-indentation.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/odd-indentation.yaml new file mode 100644 index 0000000000..6d99b1c569 --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/odd-indentation.yaml @@ -0,0 +1,9 @@ +name: odd # extra spaces after the colon +steps: +- name: a + run: echo a +- name: b + depends: [a] + run: echo b + env: + - K: v diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/quoted-keys-and-values.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/quoted-keys-and-values.yaml new file mode 100644 index 0000000000..1c0d497ade --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/quoted-keys-and-values.yaml @@ -0,0 +1,17 @@ +name: 'quoted' +"steps": + - 'name': "first" + "run": 'echo ''single'' and "double"' + - name: second + run: "echo \"escaped\" \t tab \\ backslash" + env: + "KEY WITH SPACE": "value" + 'x': yes + ? complex key + : complex value + 0x1F: !!str 0x1F + empty: + nulls: ~ + description: "multi + line quoted" + timeout_sec: 0030 diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/router-numeric-band.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/router-numeric-band.yaml new file mode 100644 index 0000000000..996e6b1794 --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/router-numeric-band.yaml @@ -0,0 +1,11 @@ +working_dir: . +steps: + - name: pick + action: router.route + with: + value: "0.95" + routes: + "num:<0.1": [review] + "num:>0.9": [review] + - name: review + run: echo review > review.out diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/router-regex-keys.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/router-regex-keys.yaml new file mode 100644 index 0000000000..470bddc6ae --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/router-regex-keys.yaml @@ -0,0 +1,13 @@ +working_dir: . +steps: + - name: pick + action: router.route + with: + value: "404" + routes: + "re:^5\\d\\d$": [server_error] + "re:^4\\d\\d$": [client_error] + - name: server_error + run: echo 5xx > server_error.out + - name: client_error + run: echo 4xx > client_error.out diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/run-context.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/run-context.yaml new file mode 100644 index 0000000000..2c37bf8b02 --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/run-context.yaml @@ -0,0 +1,38 @@ +name: builtin_runtime_context +working_dir: . +env: + - RUN_ENV=${context.run.id} +steps: + - id: capture + run: | + case '${context.attempt.started_at}' in + ????-??-??T??:??:??Z) ;; + *) printf 'bad started_at=%s\n' '${context.attempt.started_at}' >&2; exit 1 ;; + esac + if [ -z '${context.attempt.id}' ]; then + printf 'attempt id missing\n' >&2 + exit 1 + fi + if [ '${context.attempt.id}' = '${context.run.id}' ]; then + printf 'attempt matched run id\n' >&2 + exit 1 + fi + test -f '${context.paths.log_file}' + test -d '${context.paths.work_dir}' + printf 'dag=%s\n' '${context.dag.name}' > run-context.txt + printf 'run=%s\n' '${context.run.id}' >> run-context.txt + printf 'root_name=%s\n' '${context.run.root_name}' >> run-context.txt + printf 'root_id=%s\n' '${context.run.root_id}' >> run-context.txt + printf 'root_env=%s\n' "$RUN_ENV" >> run-context.txt + printf 'trigger=%s\n' '${context.trigger.type}' >> run-context.txt + printf 'scheduled=%s\n' '${context.run.scheduled_at}' >> run-context.txt + printf 'started=%s\n' '${context.attempt.started_at}' >> run-context.txt + printf 'attempt=%s\n' '${context.attempt.id}' >> run-context.txt + printf 'step_id=%s\n' '${context.step.id}' >> run-context.txt + printf 'step_name=%s\n' '${context.step.name}' >> run-context.txt + printf 'env_run=%s\n' "$DAG_RUN_ID" >> run-context.txt + printf 'env_step=%s\n' "$DAG_RUN_STEP_NAME" >> run-context.txt + printf 'stdout=%s\n' '${context.paths.step_stdout_file}' >> run-context.txt + printf 'stderr=%s\n' '${context.paths.step_stderr_file}' >> run-context.txt + printf 'output=%s\n' '${context.paths.step_output_file}' >> run-context.txt + printf 'paths_ready=yes\n' >> run-context.txt diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/scheduler-cron.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/scheduler-cron.yaml new file mode 100644 index 0000000000..10d4df52d2 --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/scheduler-cron.yaml @@ -0,0 +1,5 @@ +name: cron-test +schedule: "*/1 * * * *" +steps: + - name: test-step + run: echo "hello" diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/script-utf8-multiline.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/script-utf8-multiline.yaml new file mode 100644 index 0000000000..965cdecda6 --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/script-utf8-multiline.yaml @@ -0,0 +1,9 @@ +working_dir: . +steps: + - id: pwsh_utf8_multiline + run: | + Write-Output "first" + $text = '台帳に伝票を3件入力(未保存)' + [System.IO.File]::WriteAllText((Join-Path (Get-Location) 'pwsh-utf8-multiline.txt'), $text, (New-Object System.Text.UTF8Encoding $false)) + with: + shell: pwsh diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/shorthand-steps.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/shorthand-steps.yaml new file mode 100644 index 0000000000..2a45273b14 --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/shorthand-steps.yaml @@ -0,0 +1,9 @@ +name: shorthand +steps: + - echo one + - echo two # shorthand step + - name: three + run: echo three + - ./script.sh --flag "quoted arg" + - 'echo single quoted' + - "echo double quoted" diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/spec-harness-comments.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/spec-harness-comments.yaml new file mode 100644 index 0000000000..4a4c184131 --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/spec-harness-comments.yaml @@ -0,0 +1,16 @@ +harnesses: + remote-agent: + protocol: acp + transport: http # websocket for ws:// or wss:// endpoints + url: https://acp.example.com/acp # wss://acp.example.com/acp for secure WebSocket + cwd: /srv/project + # ca_file: /path/to/private-ca.pem + headers: + Authorization: Bearer ${ACP_TOKEN} +steps: + - action: harness.run + timeout_sec: 120 + with: + provider: remote-agent + prompt: Review the project without modifying files. + output: REVIEW diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/spec-outputs-heredoc.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/spec-outputs-heredoc.yaml new file mode 100644 index 0000000000..ad25a5ff98 --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/spec-outputs-heredoc.yaml @@ -0,0 +1,15 @@ +steps: + - id: inspect + run: | + cat >> "$DAGU_OUTPUT_FILE" <<'EOF' + metadata< result.txt + pwd > pwd.txt diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/spec-xlsx-convert.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/spec-xlsx-convert.yaml new file mode 100644 index 0000000000..3ff5efe89d --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/spec-xlsx-convert.yaml @@ -0,0 +1,9 @@ +steps: + - id: export + action: xlsx.convert + with: + path: orders.xlsx + sheet: Orders + output: out/orders.csv + encoding: shift_jis + columns: [品名, 数量, 金額] diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/unicode-comments.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/unicode-comments.yaml new file mode 100644 index 0000000000..5845309fa6 --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/unicode-comments.yaml @@ -0,0 +1,10 @@ +# Commentaire en français : à é ü ñ ç +# 日本語のコメント 😀 (surrogate pair) +name: unicode +steps: + - name: 日本語の手順 # nom non ASCII + run: echo "héllo wörld 🌍" + - name: emoji + run: echo 😀 + env: + GREETING: こんにちは diff --git a/ui/src/features/visual-editor/doc/__tests__/fixtures/xlsx-japanese.yaml b/ui/src/features/visual-editor/doc/__tests__/fixtures/xlsx-japanese.yaml new file mode 100644 index 0000000000..6e52d37536 --- /dev/null +++ b/ui/src/features/visual-editor/doc/__tests__/fixtures/xlsx-japanese.yaml @@ -0,0 +1,42 @@ +name: quote-extract-japanese +working_dir: . +env: + - LLM_BASE_URL: ${LLM_BASE_URL} +llm: + provider: local + model: test-model + base_url: ${LLM_BASE_URL} +steps: + - id: form + action: xlsx.write + with: + path: quote.xlsx + header: false + style: none + rows: + - ["御見積書", null] + - ["見積番号", "Q-2026-001"] + - ["納期", "令和8年10月15日"] + - ["合計金額", "¥123,000"] + - ["納品日時", "令和8年10月15日 午後2時30分"] + - ["検収", "済"] + - id: fields + depends: form + action: xlsx.extract + with: + path: quote.xlsx + instruction: A supplier quote. Find the quote number, the delivery date, and the total amount. + schema: + type: object + properties: + quote_no: {type: string, description: 見積番号} + delivery: {type: string, format: date, description: 納期} + total: {type: number, description: 合計金額} + delivered: {type: string, format: date-time, description: 納品日時} + accepted: {type: boolean, description: 検収} + - id: out + depends: fields + action: file.write + with: + path: out.json + content: '{"quote_no": "${steps.fields.outputs.quote_no}", "delivery": "${steps.fields.outputs.delivery}", "total": ${steps.fields.outputs.total}, "delivered": "${steps.fields.outputs.delivered}", "accepted": ${steps.fields.outputs.accepted}, "cells": ${steps.fields.outputs.cells}, "source": "${steps.fields.outputs.source}"}'