@oisincoveney/pipeline 3.24.2 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -2
- package/defaults/pipeline.yaml +23 -5
- package/defaults/profiles.yaml +104 -21
- package/dist/argo-graph.js +90 -88
- package/dist/argo-submit.d.ts +110 -138
- package/dist/argo-submit.js +363 -378
- package/dist/argo-workflow.d.ts +156 -177
- package/dist/argo-workflow.js +96 -138
- package/dist/backlog.js +5 -4
- package/dist/bench/eval-report.js +21 -12
- package/dist/claude-settings-config.js +23 -31
- package/dist/claude-user-config.js +6 -16
- package/dist/cli/bootstrap-commands.js +3 -3
- package/dist/cli/doctor.d.ts +1 -1
- package/dist/cli/doctor.js +176 -171
- package/dist/cli/factory-commands.js +13 -13
- package/dist/cli/format.js +92 -86
- package/dist/cli/loop-commands.js +21 -21
- package/dist/cli/mcp-gateway-commands.js +12 -12
- package/dist/cli/plan-commands.js +85 -116
- package/dist/cli/program.d.ts +3 -3
- package/dist/cli/program.js +41 -43
- package/dist/cli/run-command.d.ts +1 -1
- package/dist/cli/run-command.js +25 -25
- package/dist/cli/run-commands.js +31 -29
- package/dist/cli/run-resolver.d.ts +1 -1
- package/dist/cli/run-resolver.js +30 -32
- package/dist/cli/run-service.d.ts +2 -3
- package/dist/cli/run-service.js +229 -239
- package/dist/cli/submit-options.js +85 -83
- package/dist/cluster-doctor.js +147 -175
- package/dist/codex-config.js +24 -30
- package/dist/commands/bench-command.js +9 -7
- package/dist/commands/coordinator-command.js +35 -0
- package/dist/commands/pipeline-command.js +13 -7
- package/dist/commands/runner-command-command.js +39 -24
- package/dist/commands/runner-node-command.js +31 -0
- package/dist/commands/ticket/complete.js +29 -33
- package/dist/commands/ticket/create.js +84 -82
- package/dist/commands/ticket/graph-check.js +11 -11
- package/dist/commands/ticket/next.js +36 -36
- package/dist/commands/ticket/sequence.js +11 -12
- package/dist/commands/ticket/shared.d.ts +5 -2
- package/dist/commands/ticket/shared.js +63 -66
- package/dist/commands/ticket/start.js +58 -67
- package/dist/commands/ticket-command.js +2 -2
- package/dist/config/defaults.d.ts +139 -140
- package/dist/config/defaults.js +81 -80
- package/dist/config/lint.js +52 -64
- package/dist/config/load.d.ts +4 -5
- package/dist/config/load.js +64 -74
- package/dist/config/schema/catalog.d.ts +2 -1
- package/dist/config/schema/catalog.js +3 -2
- package/dist/config/schema/mcp.js +46 -52
- package/dist/config/schemas.d.ts +382 -528
- package/dist/config/schemas.js +400 -398
- package/dist/config/validate.d.ts +1 -2
- package/dist/config/validate.js +236 -201
- package/dist/config.d.ts +2 -2
- package/dist/context/repo-map.js +105 -119
- package/dist/coordinator/composition.js +169 -0
- package/dist/coordinator/config.js +71 -0
- package/dist/coordinator/run-owner.js +64 -0
- package/dist/coordinator/wave-advance.js +148 -0
- package/dist/coordinator/wave-scheduler.js +50 -0
- package/dist/credentials/broker.d.ts +6 -8
- package/dist/credentials/broker.js +17 -16
- package/dist/credentials/codex-config.js +15 -19
- package/dist/credentials/file-targets.js +9 -15
- package/dist/credentials/local-codex-auth-sync.js +55 -58
- package/dist/credentials/opencode-config.js +28 -32
- package/dist/credentials/runner.js +13 -13
- package/dist/effect-sync-errors.js +28 -0
- package/dist/factory/create-experiment.js +285 -121
- package/dist/factory/exec.js +10 -12
- package/dist/factory/git-credentials.js +4 -4
- package/dist/factory/stamp-answers.js +10 -11
- package/dist/factory/template-update.js +65 -44
- package/dist/factory-lane.d.ts +26 -40
- package/dist/factory-lane.js +26 -25
- package/dist/gates.js +3 -3
- package/dist/git-remote-url.js +17 -19
- package/dist/hooks.d.ts +26 -26
- package/dist/hooks.js +20 -22
- package/dist/index.d.ts +4 -3
- package/dist/index.js +27 -36
- package/dist/install-commands/claude-code.js +100 -92
- package/dist/install-commands/host-selection.js +3 -6
- package/dist/install-commands/opencode.js +264 -259
- package/dist/install-commands/planner.js +163 -181
- package/dist/install-commands/result-format.js +1 -3
- package/dist/install-commands/shared.js +25 -42
- package/dist/install-commands/writer.js +22 -25
- package/dist/install-commands.js +8 -8
- package/dist/jscpd-output.js +29 -0
- package/dist/json-config-merge.js +14 -20
- package/dist/json-line-values.js +17 -6
- package/dist/loop/argo-poll.d.ts +0 -1
- package/dist/loop/argo-poll.js +15 -95
- package/dist/loop/backlog-records.js +1 -3
- package/dist/loop/controller-deps.js +150 -161
- package/dist/loop/controller.js +164 -186
- package/dist/loop/gh-checks.js +62 -61
- package/dist/loop/gh-runner.js +22 -25
- package/dist/loop/loop-command.js +66 -58
- package/dist/loop/loop-controller-entrypoint.js +29 -30
- package/dist/loop/merge.js +36 -40
- package/dist/loop/run-terminal-poll.js +69 -0
- package/dist/mcp/gateway-config.js +25 -29
- package/dist/mcp/gateway-doctor.js +77 -119
- package/dist/mcp/gateway-reconcile.js +17 -22
- package/dist/mcp/gateway-runtime.js +1 -3
- package/dist/mcp/host-config.js +37 -40
- package/dist/mcp/host-renderers.js +10 -18
- package/dist/mcp/repo-local-backends.js +51 -54
- package/dist/mcp/toolhive-vmcp.js +39 -39
- package/dist/model-resolver.js +29 -33
- package/dist/moka-global-config.d.ts +78 -55
- package/dist/moka-global-config.js +93 -92
- package/dist/moka-submit.d.ts +2179 -866
- package/dist/moka-submit.js +97 -99
- package/dist/opencode-project-config.js +34 -40
- package/dist/package-assets.js +1 -3
- package/dist/path-refs.js +2 -2
- package/dist/pipeline-init.js +25 -25
- package/dist/pipeline-runtime.d.ts +15 -14
- package/dist/pipeline-runtime.js +51 -50
- package/dist/planned-node.js +6 -3
- package/dist/planning/compile.d.ts +5 -5
- package/dist/planning/compile.js +140 -139
- package/dist/planning/dependency-refs.js +5 -5
- package/dist/planning/generate.d.ts +1296 -1296
- package/dist/planning/generate.js +307 -314
- package/dist/planning/graph.d.ts +0 -1
- package/dist/planning/graph.js +131 -149
- package/dist/remote/argo/model.d.ts +104 -122
- package/dist/remote/argo/model.js +205 -197
- package/dist/remote/argo/policy.js +39 -47
- package/dist/remote/argo/storage.js +81 -85
- package/dist/remote/jobs/execution-identity.js +70 -0
- package/dist/remote/jobs/job-outcome.js +108 -0
- package/dist/remote/jobs/manifest.js +96 -0
- package/dist/remote/jobs/model.d.ts +158 -0
- package/dist/remote/jobs/model.js +162 -0
- package/dist/remote/jobs/node-bootstrap.js +213 -0
- package/dist/remote/jobs/policy.js +123 -0
- package/dist/remote/jobs/reconcile.js +100 -0
- package/dist/remote/jobs/run-spec-client.js +64 -0
- package/dist/remote/jobs/run-spec-path.js +31 -0
- package/dist/remote/jobs/run-spec-server.d.ts +34 -0
- package/dist/remote/jobs/run-spec-server.js +116 -0
- package/dist/remote/jobs/submit.d.ts +23 -0
- package/dist/remote/jobs/submit.js +116 -0
- package/dist/remote/jobs/watch.js +59 -0
- package/dist/remote/submit/argo-submission.d.ts +3 -1
- package/dist/remote/submit/argo-submission.js +53 -58
- package/dist/remote/submit/compilation.d.ts +2 -0
- package/dist/remote/submit/compilation.js +46 -49
- package/dist/remote/submit/event-boundary.js +62 -76
- package/dist/remote/submit/hook-events.d.ts +6 -0
- package/dist/remote/submit/io.d.ts +7 -1
- package/dist/remote/submit/io.js +40 -43
- package/dist/remote/submit/jobs-submission.d.ts +59 -0
- package/dist/remote/submit/jobs-submission.js +97 -0
- package/dist/remote/submit/jobs-submit-result.d.ts +25 -0
- package/dist/remote/submit/jobs-submit-result.js +34 -0
- package/dist/remote/submit/options.d.ts +49 -0
- package/dist/remote/submit/options.js +27 -0
- package/dist/remote/submit/service.d.ts +40 -1
- package/dist/remote/submit/service.js +67 -49
- package/dist/run-control/command-context.js +6 -8
- package/dist/run-control/commands.js +10 -10
- package/dist/run-control/contracts.d.ts +52 -101
- package/dist/run-control/contracts.js +83 -69
- package/dist/run-control/detach.js +66 -72
- package/dist/run-control/execution-store.js +0 -0
- package/dist/run-control/file-errors.js +1 -3
- package/dist/run-control/logical-segment.js +3 -3
- package/dist/run-control/next-node.js +67 -78
- package/dist/run-control/postgres/postgres-run-control-store.js +196 -225
- package/dist/run-control/resume-command.js +34 -41
- package/dist/run-control/run-artifacts-command.js +84 -105
- package/dist/run-control/run-command-domain.js +7 -11
- package/dist/run-control/run-control-store.js +9 -9
- package/dist/run-control/run-query-command.js +73 -100
- package/dist/run-control/run-record.js +5 -3
- package/dist/run-control/run-state-lock.js +5 -5
- package/dist/run-control/runtime-event-projection.js +38 -50
- package/dist/run-control/runtime-reporter.js +86 -102
- package/dist/run-control/stop-command.js +42 -53
- package/dist/run-control/store-fs-effects.js +3 -1
- package/dist/run-control/store-manifest.js +38 -13
- package/dist/run-control/store-paths.js +3 -3
- package/dist/run-control/store.js +1 -1
- package/dist/run-control/submit-result.js +21 -21
- package/dist/run-control/supervisor.js +78 -68
- package/dist/run-control/workspace.js +2 -2
- package/dist/run-state/git-refs.js +258 -307
- package/dist/runner/opencode-excludes.js +4 -4
- package/dist/runner/subprocess-result.js +28 -37
- package/dist/runner/subprocess.js +41 -41
- package/dist/runner/timeouts.js +14 -13
- package/dist/runner-command/dynamic-command.js +46 -39
- package/dist/runner-command/environment-commands.js +91 -0
- package/dist/runner-command/finalize.js +174 -153
- package/dist/runner-command/lifecycle-context.js +115 -130
- package/dist/runner-command/lifecycle.js +78 -72
- package/dist/runner-command/pre-schedule.js +203 -223
- package/dist/runner-command/run.js +450 -448
- package/dist/runner-command/schedule-source-options.js +8 -10
- package/dist/runner-command/select-ready-wave.js +39 -30
- package/dist/runner-command/task-descriptor.js +11 -17
- package/dist/runner-command-contract.d.ts +147 -157
- package/dist/runner-command-contract.js +403 -411
- package/dist/runner-event-schema.d.ts +364 -505
- package/dist/runner-event-schema.js +187 -175
- package/dist/runner-event-sink.js +37 -43
- package/dist/runner-output.js +6 -6
- package/dist/runner.d.ts +57 -37
- package/dist/runner.js +176 -127
- package/dist/runtime/actor-ids.d.ts +1 -0
- package/dist/runtime/actor-ids.js +2 -2
- package/dist/runtime/agent-node/agent-node.js +99 -110
- package/dist/runtime/agent-node/handoff-finalization.js +23 -24
- package/dist/runtime/agent-node/model-selection.js +19 -22
- package/dist/runtime/agent-node/output-finalization.js +174 -204
- package/dist/runtime/agent-node/prompt-rendering.js +208 -251
- package/dist/runtime/agent-node/session-execution.js +43 -39
- package/dist/runtime/builtins/builtins.js +218 -284
- package/dist/runtime/changed-files/changed-files.js +29 -34
- package/dist/runtime/command-executor/command-executor.js +31 -23
- package/dist/runtime/config-error.js +1 -3
- package/dist/runtime/context/context.js +35 -37
- package/dist/runtime/contracts/contracts.d.ts +3 -0
- package/dist/runtime/contracts/index.d.ts +1 -1
- package/dist/runtime/detached-race.js +11 -14
- package/dist/runtime/drain-merge/drain-merge.js +139 -164
- package/dist/runtime/durable-store/acquisition.js +5 -3
- package/dist/runtime/durable-store/durable-store.d.ts +3 -1
- package/dist/runtime/durable-store/postgres/client.js +29 -0
- package/dist/runtime/durable-store/postgres/migrate-substrate.js +34 -29
- package/dist/runtime/durable-store/postgres/postgres-store.js +18 -20
- package/dist/runtime/events/events.js +273 -331
- package/dist/runtime/events/index.js +2 -2
- package/dist/runtime/execution-envelope.js +44 -0
- package/dist/runtime/gates/adjudication/llm-judge.js +21 -24
- package/dist/runtime/gates/adjudication/structured-claim.js +15 -16
- package/dist/runtime/gates/adjudicator/adjudicator.js +27 -27
- package/dist/runtime/gates/adjudicator/index.js +2 -2
- package/dist/runtime/gates/contract.js +5 -9
- package/dist/runtime/gates/gates.js +9 -8
- package/dist/runtime/gates/kinds/acceptance/acceptance.js +68 -71
- package/dist/runtime/gates/kinds/acceptance/index.js +2 -2
- package/dist/runtime/gates/kinds/artifact/artifact.js +4 -4
- package/dist/runtime/gates/kinds/artifact/index.js +2 -2
- package/dist/runtime/gates/kinds/builtin/builtin.js +4 -4
- package/dist/runtime/gates/kinds/builtin/index.js +2 -2
- package/dist/runtime/gates/kinds/changed-files/changed-files.js +31 -35
- package/dist/runtime/gates/kinds/changed-files/index.js +2 -2
- package/dist/runtime/gates/kinds/command/command.js +3 -3
- package/dist/runtime/gates/kinds/command/index.js +2 -2
- package/dist/runtime/gates/kinds/json-schema/index.js +2 -2
- package/dist/runtime/gates/kinds/json-schema/json-schema.js +7 -6
- package/dist/runtime/gates/kinds/verdict/index.js +2 -2
- package/dist/runtime/gates/kinds/verdict/verdict.js +3 -3
- package/dist/runtime/gates/orchestrator.js +72 -86
- package/dist/runtime/gates/registry.js +1 -3
- package/dist/runtime/handoff.d.ts +2 -1
- package/dist/runtime/handoff.js +43 -46
- package/dist/runtime/hooks/command-hook.js +57 -71
- package/dist/runtime/hooks/context.js +21 -25
- package/dist/runtime/hooks/events.js +84 -100
- package/dist/runtime/hooks/execution.js +6 -12
- package/dist/runtime/hooks/hooks.js +34 -23
- package/dist/runtime/hooks/invocation.js +23 -29
- package/dist/runtime/hooks/module-hook.js +36 -35
- package/dist/runtime/hooks/policy.js +32 -29
- package/dist/runtime/hooks/results.js +56 -66
- package/dist/runtime/journal-acquisition.d.ts +3 -2
- package/dist/runtime/journal-acquisition.js +14 -14
- package/dist/runtime/json-validation/json-validation.js +51 -72
- package/dist/runtime/launch-plan-args.js +39 -0
- package/dist/runtime/local-scheduler.js +46 -48
- package/dist/runtime/node-execution.js +376 -445
- package/dist/runtime/node-protocol/node-protocol.js +36 -79
- package/dist/runtime/node-state-store.d.ts +4 -1
- package/dist/runtime/node-state-store.js +13 -57
- package/dist/runtime/node-state-tracker.js +46 -54
- package/dist/runtime/open-pull-request/open-pull-request.js +201 -187
- package/dist/runtime/opencode-adapter.js +23 -24
- package/dist/runtime/opencode-agent-name.js +6 -6
- package/dist/runtime/opencode-runtime.js +115 -93
- package/dist/runtime/opencode-server.js +39 -53
- package/dist/runtime/opencode-session-executor.js +312 -344
- package/dist/runtime/parallel-node/parallel-node.js +123 -134
- package/dist/runtime/parallel-worktrees/parallel-worktrees.js +90 -73
- package/dist/runtime/protected-paths/protected-paths.js +70 -71
- package/dist/runtime/remediation/remediation.js +202 -231
- package/dist/runtime/retry.js +8 -10
- package/dist/runtime/run-journal.d.ts +2 -0
- package/dist/runtime/run-journal.js +7 -7
- package/dist/runtime/runtime-results.js +54 -69
- package/dist/runtime/scheduled-dependencies.d.ts +2 -1
- package/dist/runtime/scheduled-dependencies.js +61 -75
- package/dist/runtime/scheduler.js +179 -202
- package/dist/runtime/services/agent-node-runtime-service.js +3 -3
- package/dist/runtime/services/backlog-service.d.ts +1 -2
- package/dist/runtime/services/backlog-service.js +8 -14
- package/dist/runtime/services/command-executor-service.js +1 -1
- package/dist/runtime/services/config-io-service.js +24 -21
- package/dist/runtime/services/drain-merge-git-service.js +1 -1
- package/dist/runtime/services/file-system-service.js +9 -7
- package/dist/runtime/services/git-porcelain-service.js +6 -6
- package/dist/runtime/services/kubernetes-argo-service.d.ts +10 -3
- package/dist/runtime/services/kubernetes-argo-service.js +90 -84
- package/dist/runtime/services/kubernetes-jobs-read-service.js +115 -0
- package/dist/runtime/services/kubernetes-jobs-service.d.ts +37 -0
- package/dist/runtime/services/kubernetes-jobs-service.js +45 -0
- package/dist/runtime/services/kubernetes-jobs-watch-service.js +61 -0
- package/dist/runtime/services/kubernetes-lease-service.js +21 -0
- package/dist/runtime/services/mcp-gateway-service.js +143 -94
- package/dist/runtime/services/open-pull-request-git-service.js +1 -3
- package/dist/runtime/services/opencode-runtime-server-service.js +10 -12
- package/dist/runtime/services/opencode-sdk-service.js +7 -7
- package/dist/runtime/services/repo-io-service.js +10 -12
- package/dist/runtime/services/run-journal-file-service.js +2 -2
- package/dist/runtime/services/runner-command-io-service.js +48 -52
- package/dist/runtime/services/runner-event-sink-http-service.js +32 -39
- package/dist/runtime/step/step-node.js +15 -15
- package/dist/runtime/workflow-execution.js +56 -57
- package/dist/runtime/workflow-lifecycle.js +82 -101
- package/dist/runtime/yeet-event-record.js +104 -0
- package/dist/runtime/yeet-executor.js +184 -0
- package/dist/safe-json.js +16 -16
- package/dist/schedule/backlog-context.js +122 -116
- package/dist/schedule/baseline.js +249 -255
- package/dist/schedule/passes/coverage.js +63 -68
- package/dist/schedule/passes/drain-merge.js +34 -40
- package/dist/schedule/passes/ids.js +15 -17
- package/dist/schedule/passes/models.js +40 -37
- package/dist/schedule/passes/open-pull-request.js +16 -20
- package/dist/schedule/passes/references.js +18 -22
- package/dist/schedule/prompts.js +135 -141
- package/dist/schedule/scheduling-roles.js +8 -14
- package/dist/schema-boundary.d.ts +4 -0
- package/dist/schema-boundary.js +145 -0
- package/dist/serialized-write-queue.js +2 -2
- package/dist/standard-output-schemas.js +61 -59
- package/dist/strings.js +12 -12
- package/dist/task-ref.js +5 -5
- package/dist/tickets/apply-ticket-plan.js +52 -61
- package/dist/tickets/backlog-task-store.d.ts +15 -23
- package/dist/tickets/backlog-task-store.js +82 -94
- package/dist/tickets/completion/complete-ticket.js +53 -59
- package/dist/tickets/ticket-graph-dto.d.ts +24 -48
- package/dist/tickets/ticket-graph-dto.js +40 -40
- package/dist/tickets/ticket-graph.d.ts +7 -2
- package/dist/tickets/ticket-graph.js +36 -50
- package/dist/tickets/ticket-plan-command-args.js +6 -10
- package/dist/tickets/ticket-plan-render.js +10 -16
- package/dist/tickets/ticket-plan.d.ts +32 -41
- package/dist/tickets/ticket-plan.js +97 -77
- package/dist/tickets/ticket-selection.d.ts +3 -1
- package/dist/tickets/ticket-selection.js +37 -52
- package/dist/tickets/ticket-task-index.js +13 -17
- package/dist/tickets/validation-error-format.js +3 -10
- package/dist/token-estimator.js +12 -7
- package/dist/unknown-fields.js +21 -0
- package/dist/workflow-submit-contract.d.ts +9 -10
- package/dist/workflow-submit-contract.js +10 -9
- package/docs/adr-moka-postgres-schema-isolation.md +48 -0
- package/docs/adr-opencode-first-goal-loop-runtime.md +11 -11
- package/docs/config-architecture.md +13 -13
- package/docs/mcp-host-isolation.md +1 -0
- package/docs/moka-orchestrator-design.md +3 -3
- package/docs/operator-guide.md +1 -1
- package/docs/pipeline-console-runner-contract.md +10 -6
- package/docs/runtime-actor-model.md +18 -18
- package/docs/runtime-test-subsystems.md +200 -0
- package/docs/slash-command-adapter-contract.md +3 -3
- package/package.json +49 -85
- package/dist/config/schema/reference-validation.js +0 -83
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseResultWithSchema } from "../../schema-boundary.js";
|
|
2
|
+
import { PipelineConfigError, configIssuesFromSchemaIssues, validationError } from "../../config/schemas.js";
|
|
3
|
+
import { throwCauseFailure, unknownErrorMessage } from "../../effect-sync-errors.js";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Option$1 from "effect/Option";
|
|
2
6
|
import { parseDocument } from "yaml";
|
|
3
|
-
import { Cause, Context, Effect, Layer, Option } from "effect";
|
|
4
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import * as Context from "effect/Context";
|
|
9
|
+
import * as Layer from "effect/Layer";
|
|
5
10
|
//#region src/runtime/services/config-io-service.ts
|
|
6
|
-
|
|
11
|
+
const parseYamlSource = (source, sourcePath) => {
|
|
7
12
|
const document = parseDocument(source, {
|
|
8
13
|
prettyErrors: false,
|
|
9
14
|
uniqueKeys: true
|
|
@@ -13,30 +18,28 @@ function parseYamlSource(source, sourcePath) {
|
|
|
13
18
|
path: sourcePath
|
|
14
19
|
})));
|
|
15
20
|
return document.toJS();
|
|
16
|
-
}
|
|
21
|
+
};
|
|
17
22
|
var ConfigIoService = class extends Context.Service()("ConfigIoService") {};
|
|
18
23
|
const ConfigIoServiceLive = Layer.succeed(ConfigIoService, {
|
|
19
24
|
parseYaml: (source, sourcePath) => Effect.try({
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
catch: (error) => error instanceof PipelineConfigError ? error : new PipelineConfigError("PIPELINE_CONFIG_PARSE_ERROR", `Failed to parse ${sourcePath}`, [{
|
|
26
|
+
message: unknownErrorMessage(error),
|
|
27
|
+
path: sourcePath
|
|
28
|
+
}]),
|
|
29
|
+
try: () => parseYamlSource(source, sourcePath)
|
|
22
30
|
}),
|
|
23
|
-
readOptionalText: (path) => Effect.sync(() => existsSync(path) ? readFileSync(path, "
|
|
24
|
-
readText: (path) => Effect.sync(() => readFileSync(path, "
|
|
31
|
+
readOptionalText: (path) => Effect.sync(() => existsSync(path) ? Option$1.some(readFileSync(path, "utf-8")) : Option$1.none()),
|
|
32
|
+
readText: (path) => Effect.sync(() => readFileSync(path, "utf-8"))
|
|
25
33
|
});
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
function runConfigIoSync(program) {
|
|
34
|
+
const parseConfigYamlAs = (source, sourcePath, schema) => Effect.gen(function* effectBody() {
|
|
35
|
+
const parsed = parseResultWithSchema(schema, yield* (yield* ConfigIoService).parseYaml(source, sourcePath), { onExcessProperty: "error" });
|
|
36
|
+
if (!parsed.ok) return yield* Effect.fail(validationError(configIssuesFromSchemaIssues(parsed.issues)));
|
|
37
|
+
return parsed.value;
|
|
38
|
+
});
|
|
39
|
+
const runConfigIoSync = (program) => {
|
|
35
40
|
const exit = Effect.runSyncExit(Effect.provide(program, ConfigIoServiceLive));
|
|
36
41
|
if (exit._tag === "Success") return exit.value;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
throw Cause.squash(exit.cause);
|
|
40
|
-
}
|
|
42
|
+
return throwCauseFailure(exit.cause);
|
|
43
|
+
};
|
|
41
44
|
//#endregion
|
|
42
45
|
export { ConfigIoService, parseConfigYamlAs, runConfigIoSync };
|
|
@@ -4,7 +4,7 @@ import simpleGit from "simple-git";
|
|
|
4
4
|
var DrainMergeGitService = class extends Context.Service()("DrainMergeGitService") {};
|
|
5
5
|
const DrainMergeGitServiceLive = Layer.succeed(DrainMergeGitService, { create: (baseDir) => Effect.sync(() => {
|
|
6
6
|
const git = simpleGit({ baseDir });
|
|
7
|
-
return { raw: (args) => Effect.tryPromise(() => git.raw(args)) };
|
|
7
|
+
return { raw: (args) => Effect.tryPromise(async () => await git.raw(args)) };
|
|
8
8
|
}) });
|
|
9
9
|
//#endregion
|
|
10
10
|
export { DrainMergeGitService, DrainMergeGitServiceLive };
|
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
import { Cause, Context, Effect, Layer, Option } from "effect";
|
|
2
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { Cause, Context, Effect, Layer, Option } from "effect";
|
|
3
3
|
//#region src/runtime/services/file-system-service.ts
|
|
4
4
|
var FileSystemService = class extends Context.Service()("FileSystemService") {};
|
|
5
5
|
const FileSystemServiceLive = Layer.succeed(FileSystemService, {
|
|
6
6
|
exists: (path) => Effect.sync(() => existsSync(path)),
|
|
7
7
|
readText: (path) => Effect.try({
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
catch: (error) => error,
|
|
9
|
+
try: () => readFileSync(path, "utf-8")
|
|
10
10
|
}),
|
|
11
11
|
writeText: (path, contents) => Effect.try({
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
catch: (error) => error,
|
|
13
|
+
try: () => {
|
|
14
|
+
writeFileSync(path, contents);
|
|
15
|
+
}
|
|
14
16
|
})
|
|
15
17
|
});
|
|
16
|
-
|
|
18
|
+
const runFileSystemSync = (program, layer) => {
|
|
17
19
|
const exit = Effect.runSyncExit(Effect.provide(program, layer));
|
|
18
20
|
switch (exit._tag) {
|
|
19
21
|
case "Success": return exit.value;
|
|
@@ -24,6 +26,6 @@ function runFileSystemSync(program, layer) {
|
|
|
24
26
|
}
|
|
25
27
|
default: throw new Error("unreachable Effect exit state");
|
|
26
28
|
}
|
|
27
|
-
}
|
|
29
|
+
};
|
|
28
30
|
//#endregion
|
|
29
31
|
export { FileSystemService, FileSystemServiceLive, runFileSystemSync };
|
|
@@ -6,17 +6,18 @@ const execGit = promisify(execFile);
|
|
|
6
6
|
var GitPorcelainService = class extends Context.Service()("GitPorcelainService") {};
|
|
7
7
|
const GitPorcelainServiceLive = Layer.succeed(GitPorcelainService, {
|
|
8
8
|
run: (cwd, args, env) => Effect.tryPromise({
|
|
9
|
+
catch: (error) => error,
|
|
9
10
|
try: async () => {
|
|
10
11
|
const { stdout } = await execGit("git", args, {
|
|
11
12
|
cwd,
|
|
12
|
-
encoding: "
|
|
13
|
+
encoding: "utf-8",
|
|
13
14
|
env
|
|
14
15
|
});
|
|
15
16
|
return stdout;
|
|
16
|
-
}
|
|
17
|
-
catch: (error) => error
|
|
17
|
+
}
|
|
18
18
|
}),
|
|
19
19
|
statusPorcelain: (cwd) => Effect.try({
|
|
20
|
+
catch: (error) => error,
|
|
20
21
|
try: () => execFileSync("git", [
|
|
21
22
|
"status",
|
|
22
23
|
"--porcelain=v1",
|
|
@@ -24,14 +25,13 @@ const GitPorcelainServiceLive = Layer.succeed(GitPorcelainService, {
|
|
|
24
25
|
"-z"
|
|
25
26
|
], {
|
|
26
27
|
cwd,
|
|
27
|
-
encoding: "
|
|
28
|
+
encoding: "utf-8",
|
|
28
29
|
stdio: [
|
|
29
30
|
"ignore",
|
|
30
31
|
"pipe",
|
|
31
32
|
"ignore"
|
|
32
33
|
]
|
|
33
|
-
})
|
|
34
|
-
catch: (error) => error
|
|
34
|
+
})
|
|
35
35
|
})
|
|
36
36
|
});
|
|
37
37
|
//#endregion
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { WorkflowReadApi } from "../../loop/argo-poll.js";
|
|
2
2
|
import { Context, Effect, Layer } from "effect";
|
|
3
|
-
import { CoreV1Api,
|
|
4
|
-
|
|
3
|
+
import { CoreV1Api, KubeConfig } from "@kubernetes/client-node";
|
|
5
4
|
//#region src/runtime/services/kubernetes-argo-service.d.ts
|
|
6
5
|
interface CoreApi {
|
|
7
6
|
readonly createNamespacedConfigMap: (param: Parameters<CoreV1Api["createNamespacedConfigMap"]>[0], options?: Parameters<CoreV1Api["createNamespacedConfigMap"]>[1]) => Promise<unknown>;
|
|
8
7
|
readonly deleteNamespacedConfigMap: (param: Parameters<CoreV1Api["deleteNamespacedConfigMap"]>[0], options?: Parameters<CoreV1Api["deleteNamespacedConfigMap"]>[1]) => Promise<unknown>;
|
|
9
8
|
readonly patchNamespacedConfigMap: (param: Parameters<CoreV1Api["patchNamespacedConfigMap"]>[0], options?: Parameters<CoreV1Api["patchNamespacedConfigMap"]>[1]) => Promise<unknown>;
|
|
10
9
|
}
|
|
11
|
-
|
|
10
|
+
interface WorkflowApi {
|
|
11
|
+
readonly createNamespacedCustomObject: (param: {
|
|
12
|
+
readonly body: unknown;
|
|
13
|
+
readonly group: string;
|
|
14
|
+
readonly namespace: string;
|
|
15
|
+
readonly plural: string;
|
|
16
|
+
readonly version: string;
|
|
17
|
+
}) => Promise<unknown>;
|
|
18
|
+
}
|
|
12
19
|
interface KubernetesArgoIoDependencies {
|
|
13
20
|
coreApi?: CoreApi;
|
|
14
21
|
kubeConfig?: KubeConfig;
|
|
@@ -1,73 +1,110 @@
|
|
|
1
|
+
import { stringField } from "../../unknown-fields.js";
|
|
1
2
|
import { classifyArgoPhase, extractArgoRawPhase } from "../../loop/argo-poll.js";
|
|
2
|
-
import {
|
|
3
|
+
import { getOrElse, isSome, none, some } from "effect/Option";
|
|
3
4
|
import { execa } from "execa";
|
|
5
|
+
import { Context, Effect, Layer } from "effect";
|
|
4
6
|
import { CoreV1Api, CustomObjectsApi, KubeConfig, PatchStrategy, setHeaderOptions } from "@kubernetes/client-node";
|
|
5
7
|
//#region src/runtime/services/kubernetes-argo-service.ts
|
|
8
|
+
const textOption = (value) => value === void 0 || value.length === 0 ? none() : some(value);
|
|
6
9
|
var KubernetesArgoService = class extends Context.Service()("KubernetesArgoService") {};
|
|
10
|
+
const resolveKubeConfig = (options, dependencies) => {
|
|
11
|
+
if (dependencies.kubeConfig !== void 0) return dependencies.kubeConfig;
|
|
12
|
+
const kubeConfig = new KubeConfig();
|
|
13
|
+
const kubeconfigPath = textOption(options.kubeconfigPath);
|
|
14
|
+
if (isSome(kubeconfigPath)) kubeConfig.loadFromFile(kubeconfigPath.value);
|
|
15
|
+
else kubeConfig.loadFromDefault();
|
|
16
|
+
const kubeContext = textOption(options.kubeContext);
|
|
17
|
+
if (isSome(kubeContext)) {
|
|
18
|
+
if (kubeConfig.getContextObject(kubeContext.value) === null) throw new Error(`Kube context '${kubeContext.value}' was not found in the resolved kubeconfig`);
|
|
19
|
+
kubeConfig.setCurrentContext(kubeContext.value);
|
|
20
|
+
}
|
|
21
|
+
return kubeConfig;
|
|
22
|
+
};
|
|
23
|
+
const buildApiClients = (kubeConfig, dependencies) => ({
|
|
24
|
+
coreApi: dependencies.coreApi ?? kubeConfig.makeApiClient(CoreV1Api),
|
|
25
|
+
workflowApi: dependencies.workflowApi ?? kubeConfig.makeApiClient(CustomObjectsApi)
|
|
26
|
+
});
|
|
27
|
+
const apiClients = (options, dependencies) => {
|
|
28
|
+
if (dependencies.coreApi !== void 0 && dependencies.workflowApi !== void 0) return {
|
|
29
|
+
coreApi: dependencies.coreApi,
|
|
30
|
+
workflowApi: dependencies.workflowApi
|
|
31
|
+
};
|
|
32
|
+
return buildApiClients(resolveKubeConfig(options, dependencies), dependencies);
|
|
33
|
+
};
|
|
34
|
+
const readApiClient = (options, dependencies) => {
|
|
35
|
+
if (dependencies.workflowReadApi !== void 0) return dependencies.workflowReadApi;
|
|
36
|
+
return resolveKubeConfig(options, dependencies).makeApiClient(CustomObjectsApi);
|
|
37
|
+
};
|
|
38
|
+
const kubectlArgs = (args, kubeContext) => {
|
|
39
|
+
const context = textOption(kubeContext);
|
|
40
|
+
return isSome(context) ? [
|
|
41
|
+
"--context",
|
|
42
|
+
context.value,
|
|
43
|
+
...args
|
|
44
|
+
] : [...args];
|
|
45
|
+
};
|
|
46
|
+
const kubectlErrorStderr = (error) => {
|
|
47
|
+
const stderr = stringField(error, "stderr");
|
|
48
|
+
if (isSome(stderr)) return stderr.value.trim();
|
|
49
|
+
return getOrElse(stringField(error, "shortMessage"), () => "kubectl failed").trim();
|
|
50
|
+
};
|
|
51
|
+
const kubectlErrorStdout = (error) => getOrElse(stringField(error, "stdout"), () => "").trim();
|
|
7
52
|
const KubernetesArgoServiceLive = Layer.succeed(KubernetesArgoService, {
|
|
8
53
|
createConfigMap: ({ body, dependencies, namespace, options }) => Effect.try({
|
|
9
|
-
|
|
10
|
-
|
|
54
|
+
catch: (error) => error,
|
|
55
|
+
try: () => apiClients(options, dependencies)
|
|
11
56
|
}).pipe(Effect.flatMap(({ coreApi }) => Effect.tryPromise({
|
|
12
|
-
|
|
57
|
+
catch: (error) => error,
|
|
58
|
+
try: async () => await coreApi.createNamespacedConfigMap({
|
|
13
59
|
body,
|
|
14
60
|
namespace
|
|
15
|
-
})
|
|
16
|
-
catch: (error) => error
|
|
17
|
-
}))),
|
|
18
|
-
deleteConfigMap: ({ dependencies, name, namespace, options }) => Effect.try({
|
|
19
|
-
try: () => apiClients(options, dependencies),
|
|
20
|
-
catch: (error) => error
|
|
21
|
-
}).pipe(Effect.flatMap(({ coreApi }) => Effect.tryPromise({
|
|
22
|
-
try: () => coreApi.deleteNamespacedConfigMap({
|
|
23
|
-
name,
|
|
24
|
-
namespace
|
|
25
|
-
}),
|
|
26
|
-
catch: (error) => error
|
|
61
|
+
})
|
|
27
62
|
}))),
|
|
28
63
|
createWorkflow: ({ body, dependencies, namespace, options }) => Effect.try({
|
|
29
|
-
|
|
30
|
-
|
|
64
|
+
catch: (error) => error,
|
|
65
|
+
try: () => apiClients(options, dependencies)
|
|
31
66
|
}).pipe(Effect.flatMap(({ workflowApi }) => Effect.tryPromise({
|
|
32
|
-
|
|
67
|
+
catch: (error) => error,
|
|
68
|
+
try: async () => await workflowApi.createNamespacedCustomObject({
|
|
33
69
|
body,
|
|
34
70
|
group: "argoproj.io",
|
|
35
71
|
namespace,
|
|
36
72
|
plural: "workflows",
|
|
37
73
|
version: "v1alpha1"
|
|
38
|
-
})
|
|
39
|
-
|
|
74
|
+
})
|
|
75
|
+
}))),
|
|
76
|
+
deleteConfigMap: ({ dependencies, name, namespace, options }) => Effect.try({
|
|
77
|
+
catch: (error) => error,
|
|
78
|
+
try: () => apiClients(options, dependencies)
|
|
79
|
+
}).pipe(Effect.flatMap(({ coreApi }) => Effect.tryPromise({
|
|
80
|
+
catch: (error) => error,
|
|
81
|
+
try: async () => await coreApi.deleteNamespacedConfigMap({
|
|
82
|
+
name,
|
|
83
|
+
namespace
|
|
84
|
+
})
|
|
40
85
|
}))),
|
|
41
86
|
getWorkflowPhase: ({ dependencies, name, namespace, options }) => Effect.try({
|
|
42
|
-
|
|
43
|
-
|
|
87
|
+
catch: (error) => error,
|
|
88
|
+
try: () => readApiClient(options, dependencies)
|
|
44
89
|
}).pipe(Effect.flatMap((workflowReadApi) => Effect.tryPromise({
|
|
45
|
-
|
|
90
|
+
catch: (error) => error,
|
|
91
|
+
try: async () => await workflowReadApi.getNamespacedCustomObject({
|
|
46
92
|
group: "argoproj.io",
|
|
47
93
|
name,
|
|
48
94
|
namespace,
|
|
49
95
|
plural: "workflows",
|
|
50
96
|
version: "v1alpha1"
|
|
51
|
-
})
|
|
52
|
-
catch: (error) => error
|
|
97
|
+
})
|
|
53
98
|
})), Effect.map((resource) => classifyArgoPhase(extractArgoRawPhase(resource)))),
|
|
54
|
-
patchConfigMapOwnerReferences: ({ body, dependencies, name, namespace, options }) => Effect.try({
|
|
55
|
-
try: () => apiClients(options, dependencies),
|
|
56
|
-
catch: (error) => error
|
|
57
|
-
}).pipe(Effect.flatMap(({ coreApi }) => Effect.tryPromise({
|
|
58
|
-
try: () => coreApi.patchNamespacedConfigMap({
|
|
59
|
-
body,
|
|
60
|
-
name,
|
|
61
|
-
namespace
|
|
62
|
-
}, setHeaderOptions("Content-Type", PatchStrategy.MergePatch)),
|
|
63
|
-
catch: (error) => error
|
|
64
|
-
}))),
|
|
65
99
|
kubectl: (args, options) => Effect.tryPromise({
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
100
|
+
catch: (error) => error,
|
|
101
|
+
try: async () => {
|
|
102
|
+
const kubeconfigPath = textOption(options.kubeconfigPath);
|
|
103
|
+
return await execa("kubectl", kubectlArgs(args, options.kubeContext), {
|
|
104
|
+
env: isSome(kubeconfigPath) ? { KUBECONFIG: kubeconfigPath.value } : void 0,
|
|
105
|
+
stdin: "ignore"
|
|
106
|
+
});
|
|
107
|
+
}
|
|
71
108
|
}).pipe(Effect.map((result) => ({
|
|
72
109
|
ok: true,
|
|
73
110
|
stderr: result.stderr,
|
|
@@ -76,49 +113,18 @@ const KubernetesArgoServiceLive = Layer.succeed(KubernetesArgoService, {
|
|
|
76
113
|
ok: false,
|
|
77
114
|
stderr: kubectlErrorStderr(error),
|
|
78
115
|
stdout: kubectlErrorStdout(error)
|
|
116
|
+
}))),
|
|
117
|
+
patchConfigMapOwnerReferences: ({ body, dependencies, name, namespace, options }) => Effect.try({
|
|
118
|
+
catch: (error) => error,
|
|
119
|
+
try: () => apiClients(options, dependencies)
|
|
120
|
+
}).pipe(Effect.flatMap(({ coreApi }) => Effect.tryPromise({
|
|
121
|
+
catch: (error) => error,
|
|
122
|
+
try: async () => await coreApi.patchNamespacedConfigMap({
|
|
123
|
+
body,
|
|
124
|
+
name,
|
|
125
|
+
namespace
|
|
126
|
+
}, setHeaderOptions("Content-Type", PatchStrategy.MergePatch))
|
|
79
127
|
})))
|
|
80
128
|
});
|
|
81
|
-
function resolveKubeConfig(options, dependencies) {
|
|
82
|
-
if (dependencies.kubeConfig) return dependencies.kubeConfig;
|
|
83
|
-
const kubeConfig = new KubeConfig();
|
|
84
|
-
if (options.kubeconfigPath) kubeConfig.loadFromFile(options.kubeconfigPath);
|
|
85
|
-
else kubeConfig.loadFromDefault();
|
|
86
|
-
if (options.kubeContext) {
|
|
87
|
-
if (!kubeConfig.getContextObject(options.kubeContext)) throw new Error(`Kube context '${options.kubeContext}' was not found in the resolved kubeconfig`);
|
|
88
|
-
kubeConfig.setCurrentContext(options.kubeContext);
|
|
89
|
-
}
|
|
90
|
-
return kubeConfig;
|
|
91
|
-
}
|
|
92
|
-
function apiClients(options, dependencies) {
|
|
93
|
-
if (dependencies.coreApi && dependencies.workflowApi) return {
|
|
94
|
-
coreApi: dependencies.coreApi,
|
|
95
|
-
workflowApi: dependencies.workflowApi
|
|
96
|
-
};
|
|
97
|
-
return buildApiClients(resolveKubeConfig(options, dependencies), dependencies);
|
|
98
|
-
}
|
|
99
|
-
function buildApiClients(kubeConfig, dependencies) {
|
|
100
|
-
return {
|
|
101
|
-
coreApi: dependencies.coreApi ?? kubeConfig.makeApiClient(CoreV1Api),
|
|
102
|
-
workflowApi: dependencies.workflowApi ?? kubeConfig.makeApiClient(CustomObjectsApi)
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
function readApiClient(options, dependencies) {
|
|
106
|
-
if (dependencies.workflowReadApi) return dependencies.workflowReadApi;
|
|
107
|
-
return resolveKubeConfig(options, dependencies).makeApiClient(CustomObjectsApi);
|
|
108
|
-
}
|
|
109
|
-
function kubectlArgs(args, kubeContext) {
|
|
110
|
-
return kubeContext ? [
|
|
111
|
-
"--context",
|
|
112
|
-
kubeContext,
|
|
113
|
-
...args
|
|
114
|
-
] : [...args];
|
|
115
|
-
}
|
|
116
|
-
function kubectlErrorStderr(error) {
|
|
117
|
-
const parsed = error;
|
|
118
|
-
return (parsed.stderr || parsed.shortMessage || "kubectl failed").trim();
|
|
119
|
-
}
|
|
120
|
-
function kubectlErrorStdout(error) {
|
|
121
|
-
return (error.stdout || "").trim();
|
|
122
|
-
}
|
|
123
129
|
//#endregion
|
|
124
130
|
export { KubernetesArgoService, KubernetesArgoServiceLive, resolveKubeConfig };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { isRecord } from "../../safe-json.js";
|
|
2
|
+
import { numberField, stringField } from "../../unknown-fields.js";
|
|
3
|
+
import { resolveKubeConfig } from "./kubernetes-argo-service.js";
|
|
4
|
+
import { JOB_LABEL_NODE, JOB_LABEL_RUN_ID } from "../../remote/jobs/policy.js";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
import * as Option$1 from "effect/Option";
|
|
7
|
+
import * as R from "effect/Record";
|
|
8
|
+
import * as Arr from "effect/Array";
|
|
9
|
+
import { BatchV1Api, CoreV1Api } from "@kubernetes/client-node";
|
|
10
|
+
//#region src/runtime/services/kubernetes-jobs-read-service.ts
|
|
11
|
+
const field = (value, key) => isRecord(value) ? value[key] : void 0;
|
|
12
|
+
const nodeIdOf = (item) => stringField(field(field(item, "metadata"), "labels"), JOB_LABEL_NODE);
|
|
13
|
+
const numberOr = (value, key) => Option$1.getOrElse(numberField(value, key), () => 0);
|
|
14
|
+
const stringOr = (value, key) => Option$1.getOrElse(stringField(value, key), () => "");
|
|
15
|
+
const toCondition = (raw) => ({
|
|
16
|
+
...Option$1.match(stringField(raw, "reason"), {
|
|
17
|
+
onNone: () => ({}),
|
|
18
|
+
onSome: (reason) => ({ reason })
|
|
19
|
+
}),
|
|
20
|
+
status: stringOr(raw, "status"),
|
|
21
|
+
type: stringOr(raw, "type")
|
|
22
|
+
});
|
|
23
|
+
const toObservedJob = (item) => {
|
|
24
|
+
const status = field(item, "status");
|
|
25
|
+
const conditions = field(status, "conditions");
|
|
26
|
+
return {
|
|
27
|
+
active: numberOr(status, "active"),
|
|
28
|
+
conditions: Arr.isArray(conditions) ? conditions.map(toCondition) : [],
|
|
29
|
+
failed: numberOr(status, "failed"),
|
|
30
|
+
succeeded: numberOr(status, "succeeded")
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
const toObservedPod = (item) => {
|
|
34
|
+
const status = field(item, "status");
|
|
35
|
+
return {
|
|
36
|
+
phase: stringOr(status, "phase"),
|
|
37
|
+
...Option$1.match(stringField(status, "reason"), {
|
|
38
|
+
onNone: () => ({}),
|
|
39
|
+
onSome: (reason) => ({ reason })
|
|
40
|
+
})
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
const pair = (key, value) => [key, value];
|
|
44
|
+
const itemsOf = (list) => {
|
|
45
|
+
const items = field(list, "items");
|
|
46
|
+
return Arr.isArray(items) ? items : [];
|
|
47
|
+
};
|
|
48
|
+
const nodeEntries = (items, project) => items.flatMap((item) => Arr.fromOption(Option$1.map(nodeIdOf(item), (nodeId) => pair(nodeId, project(item)))));
|
|
49
|
+
/**
|
|
50
|
+
* Fold a run's raw `V1JobList` + `V1PodList` into the per-node observation the
|
|
51
|
+
* reconcile classifier consumes. Each Job/pod is keyed by its `node` label; a
|
|
52
|
+
* node that has pods but no Job (Job GC'd while a pod lingers) surfaces with an
|
|
53
|
+
* absent `job`, which the classifier treats as the dead-Job signal.
|
|
54
|
+
*/
|
|
55
|
+
const parseRunObservation = (jobList, podList) => {
|
|
56
|
+
const jobsByNode = R.fromEntries(nodeEntries(itemsOf(jobList), toObservedJob));
|
|
57
|
+
const podPairs = nodeEntries(itemsOf(podList), toObservedPod);
|
|
58
|
+
const podsByNode = Arr.groupBy(podPairs, ([nodeId]) => nodeId);
|
|
59
|
+
const nodeIds = Arr.dedupe([...R.keys(jobsByNode), ...R.keys(podsByNode)]);
|
|
60
|
+
return { nodes: R.fromEntries(nodeIds.map((nodeId) => {
|
|
61
|
+
const pods = Option$1.match(R.get(podsByNode, nodeId), {
|
|
62
|
+
onNone: () => [],
|
|
63
|
+
onSome: (grouped) => grouped.map(([, pod]) => pod)
|
|
64
|
+
});
|
|
65
|
+
const observation = Option$1.match(R.get(jobsByNode, nodeId), {
|
|
66
|
+
onNone: () => ({ pods }),
|
|
67
|
+
onSome: (job) => ({
|
|
68
|
+
job,
|
|
69
|
+
pods
|
|
70
|
+
})
|
|
71
|
+
});
|
|
72
|
+
return pair(nodeId, observation);
|
|
73
|
+
})) };
|
|
74
|
+
};
|
|
75
|
+
const listApiClient = (options, dependencies) => {
|
|
76
|
+
if (dependencies.listApi !== void 0) return dependencies.listApi;
|
|
77
|
+
const kubeConfig = resolveKubeConfig(options, { kubeConfig: dependencies.kubeConfig });
|
|
78
|
+
const batchApi = kubeConfig.makeApiClient(BatchV1Api);
|
|
79
|
+
const coreApi = kubeConfig.makeApiClient(CoreV1Api);
|
|
80
|
+
return {
|
|
81
|
+
listJobs: async ({ labelSelector, namespace }) => await batchApi.listNamespacedJob({
|
|
82
|
+
labelSelector,
|
|
83
|
+
namespace
|
|
84
|
+
}),
|
|
85
|
+
listPods: async ({ labelSelector, namespace }) => await coreApi.listNamespacedPod({
|
|
86
|
+
labelSelector,
|
|
87
|
+
namespace
|
|
88
|
+
})
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Build the {@link JobFleetReadApi} the reconcile loop reads cluster truth
|
|
93
|
+
* through. Lists a run's Jobs+pods by its run-id label and folds them via
|
|
94
|
+
* {@link parseRunObservation}. Client resolution is deferred into the Effect so
|
|
95
|
+
* a kubeconfig failure surfaces as a typed error, not a constructor throw.
|
|
96
|
+
*/
|
|
97
|
+
const kubernetesJobFleetReadApi = (options, dependencies = {}) => ({ observeRun: ({ namespace, runId }) => Effect.try({
|
|
98
|
+
catch: (error) => error,
|
|
99
|
+
try: () => listApiClient(options, dependencies)
|
|
100
|
+
}).pipe(Effect.flatMap((listApi) => Effect.tryPromise({
|
|
101
|
+
catch: (error) => error,
|
|
102
|
+
try: async () => {
|
|
103
|
+
const labelSelector = `${JOB_LABEL_RUN_ID}=${runId}`;
|
|
104
|
+
const [jobList, podList] = await Promise.all([listApi.listJobs({
|
|
105
|
+
labelSelector,
|
|
106
|
+
namespace
|
|
107
|
+
}), listApi.listPods({
|
|
108
|
+
labelSelector,
|
|
109
|
+
namespace
|
|
110
|
+
})]);
|
|
111
|
+
return parseRunObservation(jobList, podList);
|
|
112
|
+
}
|
|
113
|
+
}))) });
|
|
114
|
+
//#endregion
|
|
115
|
+
export { kubernetesJobFleetReadApi, parseRunObservation };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import "./kubernetes-argo-service.js";
|
|
2
|
+
import { RunnerJobManifest } from "../../remote/jobs/model.js";
|
|
3
|
+
import { Context, Effect, Layer } from "effect";
|
|
4
|
+
import { KubeConfig } from "@kubernetes/client-node";
|
|
5
|
+
//#region src/runtime/services/kubernetes-jobs-service.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Client seam for the Jobs transport (doc-1 decision 3). Deliberately narrow:
|
|
8
|
+
* the only cluster mutation the coordinator performs to launch a node is
|
|
9
|
+
* `createNamespacedJob`. There is NO ConfigMap method here — that is the
|
|
10
|
+
* structural guarantee behind ENG-18 AC#2 ("zero per-run ConfigMaps"): the
|
|
11
|
+
* Jobs path cannot create a ConfigMap because it holds no ConfigMap client.
|
|
12
|
+
*
|
|
13
|
+
* Client injection mirrors {@link KubernetesArgoService}: tests pass a fake
|
|
14
|
+
* `batchApi`; production resolves a real {@link BatchV1Api} from the kubeconfig.
|
|
15
|
+
*/
|
|
16
|
+
interface BatchApi {
|
|
17
|
+
readonly createNamespacedJob: (param: {
|
|
18
|
+
readonly body: RunnerJobManifest;
|
|
19
|
+
readonly namespace: string;
|
|
20
|
+
}) => Promise<unknown>;
|
|
21
|
+
/**
|
|
22
|
+
* ENG-57.6: read one Job by name — the OBSERVE half of create-or-observe. When
|
|
23
|
+
* a deterministic `(run, node, generation)` create races (or replays) into an
|
|
24
|
+
* AlreadyExists, the coordinator reads the existing Job rather than creating a
|
|
25
|
+
* duplicate. Optional so pre-57.6 fakes/callers that never observe stay valid.
|
|
26
|
+
*/
|
|
27
|
+
readonly readNamespacedJob?: (param: {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly namespace: string;
|
|
30
|
+
}) => Promise<unknown>;
|
|
31
|
+
}
|
|
32
|
+
interface KubernetesJobsIoDependencies {
|
|
33
|
+
batchApi?: BatchApi;
|
|
34
|
+
kubeConfig?: KubeConfig;
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { BatchApi, KubernetesJobsIoDependencies };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { resolveKubeConfig } from "./kubernetes-argo-service.js";
|
|
2
|
+
import { Context, Effect, Layer } from "effect";
|
|
3
|
+
import { BatchV1Api } from "@kubernetes/client-node";
|
|
4
|
+
//#region src/runtime/services/kubernetes-jobs-service.ts
|
|
5
|
+
const batchApiClient = (options, dependencies) => {
|
|
6
|
+
if (dependencies.batchApi !== void 0) return dependencies.batchApi;
|
|
7
|
+
return resolveKubeConfig(options, { kubeConfig: dependencies.kubeConfig }).makeApiClient(BatchV1Api);
|
|
8
|
+
};
|
|
9
|
+
/** Raised when a create-or-observe read is required but the injected client has
|
|
10
|
+
* no read capability — a wiring error, surfaced rather than silently skipped. */
|
|
11
|
+
var JobReadUnsupportedError = class extends Error {
|
|
12
|
+
constructor() {
|
|
13
|
+
super("KubernetesJobsService read: injected BatchApi does not support readNamespacedJob");
|
|
14
|
+
this.name = "JobReadUnsupportedError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var KubernetesJobsService = class extends Context.Service()("KubernetesJobsService") {};
|
|
18
|
+
const KubernetesJobsServiceLive = Layer.succeed(KubernetesJobsService, {
|
|
19
|
+
createJob: ({ body, dependencies, namespace, options }) => Effect.try({
|
|
20
|
+
catch: (error) => error,
|
|
21
|
+
try: () => batchApiClient(options, dependencies)
|
|
22
|
+
}).pipe(Effect.flatMap((batchApi) => Effect.tryPromise({
|
|
23
|
+
catch: (error) => error,
|
|
24
|
+
try: async () => await batchApi.createNamespacedJob({
|
|
25
|
+
body,
|
|
26
|
+
namespace
|
|
27
|
+
})
|
|
28
|
+
}))),
|
|
29
|
+
readJob: ({ dependencies, name, namespace, options }) => Effect.try({
|
|
30
|
+
catch: (error) => error,
|
|
31
|
+
try: () => batchApiClient(options, dependencies)
|
|
32
|
+
}).pipe(Effect.flatMap((batchApi) => {
|
|
33
|
+
const read = batchApi.readNamespacedJob;
|
|
34
|
+
if (read === void 0) return Effect.fail(new JobReadUnsupportedError());
|
|
35
|
+
return Effect.tryPromise({
|
|
36
|
+
catch: (error) => error,
|
|
37
|
+
try: async () => await read({
|
|
38
|
+
name,
|
|
39
|
+
namespace
|
|
40
|
+
})
|
|
41
|
+
});
|
|
42
|
+
}))
|
|
43
|
+
});
|
|
44
|
+
//#endregion
|
|
45
|
+
export { JobReadUnsupportedError, KubernetesJobsService, KubernetesJobsServiceLive };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { resolveKubeConfig } from "./kubernetes-argo-service.js";
|
|
2
|
+
import { JOBS_TRANSPORT_SOURCE, JOB_LABEL_SOURCE } from "../../remote/jobs/policy.js";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Option$1 from "effect/Option";
|
|
5
|
+
import { Watch } from "@kubernetes/client-node";
|
|
6
|
+
import * as Queue from "effect/Queue";
|
|
7
|
+
//#region src/runtime/services/kubernetes-jobs-watch-service.ts
|
|
8
|
+
const watchStarterOf = (options, dependencies) => {
|
|
9
|
+
if (dependencies.watchStarter !== void 0) return dependencies.watchStarter;
|
|
10
|
+
const watch = new Watch(resolveKubeConfig(options, { kubeConfig: dependencies.kubeConfig }));
|
|
11
|
+
return async (path, queryParams, callback, done) => watch.watch(path, queryParams, callback, done);
|
|
12
|
+
};
|
|
13
|
+
const jobsPath = (namespace) => `/apis/batch/v1/namespaces/${namespace}/jobs`;
|
|
14
|
+
const podsPath = (namespace) => `/api/v1/namespaces/${namespace}/pods`;
|
|
15
|
+
/** Open one watch stream, bridging its `callback`/`done` onto `queue`, and
|
|
16
|
+
* register its `AbortController` for release on any scope exit. */
|
|
17
|
+
const openWatch = (starter, path, labelSelector, queue) => Effect.acquireRelease(Effect.tryPromise({
|
|
18
|
+
catch: (error) => error,
|
|
19
|
+
try: async () => starter(path, { labelSelector }, () => Effect.runSync(Queue.offer(queue, { kind: "event" })), (error) => Effect.runSync(Queue.offer(queue, {
|
|
20
|
+
error,
|
|
21
|
+
kind: "done"
|
|
22
|
+
})))
|
|
23
|
+
}), (controller) => Effect.sync(() => controller.abort()));
|
|
24
|
+
/** Register the caller-abort listener for release (removal) on any scope exit. */
|
|
25
|
+
const registerAbortListener = (signal, queue) => Option$1.match(signal, {
|
|
26
|
+
onNone: () => Effect.void,
|
|
27
|
+
onSome: (activeSignal) => {
|
|
28
|
+
const onAbort = () => {
|
|
29
|
+
Effect.runSync(Queue.offer(queue, { kind: "aborted" }));
|
|
30
|
+
};
|
|
31
|
+
return Effect.acquireRelease(Effect.sync(() => activeSignal.addEventListener("abort", onAbort, { once: true })), () => Effect.sync(() => activeSignal.removeEventListener("abort", onAbort)));
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
/** Drain the shared queue: run `onEvent` per event, sequenced and awaited (so a
|
|
35
|
+
* failing reconcile surfaces through the Effect failure channel, same as a
|
|
36
|
+
* transport error); stop at the first terminal message from either stream or
|
|
37
|
+
* the caller-abort listener. */
|
|
38
|
+
const drainSession = (queue, onEvent) => Effect.gen(function* drain() {
|
|
39
|
+
const message = yield* Queue.take(queue);
|
|
40
|
+
if (message.kind === "aborted") return "aborted";
|
|
41
|
+
if (message.kind === "done") {
|
|
42
|
+
if (message.error !== null && message.error !== void 0) return yield* Effect.fail(message.error);
|
|
43
|
+
return "closed";
|
|
44
|
+
}
|
|
45
|
+
yield* onEvent;
|
|
46
|
+
return yield* drainSession(queue, onEvent);
|
|
47
|
+
});
|
|
48
|
+
const kubernetesJobFleetWatchApi = (options, dependencies = {}) => ({ watchSession: ({ namespace, onEvent }) => {
|
|
49
|
+
if (dependencies.signal?.aborted === true) return Effect.succeed("aborted");
|
|
50
|
+
return Effect.scoped(Effect.gen(function* session() {
|
|
51
|
+
const starter = watchStarterOf(options, dependencies);
|
|
52
|
+
const labelSelector = `${JOB_LABEL_SOURCE}=${JOBS_TRANSPORT_SOURCE}`;
|
|
53
|
+
const queue = yield* Queue.unbounded();
|
|
54
|
+
yield* openWatch(starter, jobsPath(namespace), labelSelector, queue);
|
|
55
|
+
yield* openWatch(starter, podsPath(namespace), labelSelector, queue);
|
|
56
|
+
yield* registerAbortListener(Option$1.fromNullishOr(dependencies.signal), queue);
|
|
57
|
+
return yield* drainSession(queue, onEvent);
|
|
58
|
+
}));
|
|
59
|
+
} });
|
|
60
|
+
//#endregion
|
|
61
|
+
export { kubernetesJobFleetWatchApi };
|