Skip to content

Commit 44f03d2

Browse files
mleonidasjsbroksjsingleton-dev
authored
feat: add argo workflow JobAgent (#856)
Signed-off-by: Mike Leone <2907207+mleonidas@users.noreply.github.com> Co-authored-by: Justin Brooks <jsbroks@gmail.com> Co-authored-by: James Singleton <jsingleton@coreweave.com>
1 parent 6bf858d commit 44f03d2

18 files changed

Lines changed: 3227 additions & 1720 deletions

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ AUTH_SECRET='d0c1b54c50ccd3c89ee37e9c041f91748d361b09f8fd3b7fe542779c0f3f0983'
88
AUTH_TRUST_HOST=false
99

1010
VARIABLES_AES_256_KEY=0000000000000000000000000000000000000000000000000000000000000000
11+
ARGO_WORKFLOW_INSECURE_SKIP_VERIFY=true

apps/api/src/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export const env = createEnv({
3939
OTEL_SAMPLER_RATIO: z.number().optional().default(1),
4040

4141
AZURE_APP_CLIENT_ID: z.string().optional(),
42+
43+
ARGO_WORKFLOW_WEBHOOK_SECRET: z.string().optional(),
4244
},
4345
runtimeEnv: process.env,
4446

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { Request, Response } from "express";
2+
import { asyncHandler } from "@/types/api.js";
3+
import { Router } from "express";
4+
5+
import { eq } from "@ctrlplane/db";
6+
import { db } from "@ctrlplane/db/client";
7+
import * as schema from "@ctrlplane/db/schema";
8+
9+
import { handleArgoWorkflow } from "./workflow.js";
10+
11+
export const createArgoWorkflowRouter = (): Router =>
12+
Router().post("/:id/webhook", asyncHandler(handleWebhookRequest));
13+
14+
const getJobAgent = async (id: string) => {
15+
return db.query.jobAgent.findFirst({
16+
where: eq(schema.jobAgent.id, id),
17+
});
18+
};
19+
20+
const handleWebhookRequest = async (req: Request, res: Response) => {
21+
const { id } = req.params;
22+
if (id == null) {
23+
res.status(400).json({ message: "Missing job agent id" });
24+
return;
25+
}
26+
27+
const agent = await getJobAgent(id);
28+
if (agent == null) {
29+
res.status(404).json({ message: "Job agent not found" });
30+
return;
31+
}
32+
33+
const config = agent.config as Record<string, unknown>;
34+
const webhookSecret =
35+
typeof config.webhookSecret === "string" ? config.webhookSecret : null;
36+
if (webhookSecret == null) {
37+
res
38+
.status(500)
39+
.json({ message: "Job agent has no webhookSecret configured" });
40+
return;
41+
}
42+
43+
const authHeader = req.headers.authorization?.toString();
44+
if (authHeader == null || authHeader !== webhookSecret) {
45+
res.status(401).json({ message: "Unauthorized" });
46+
return;
47+
}
48+
49+
const payload = req.body;
50+
await handleArgoWorkflow(payload);
51+
res.status(200).send();
52+
};
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { eq } from "@ctrlplane/db";
2+
import { db } from "@ctrlplane/db/client";
3+
import { enqueueAllReleaseTargetsDesiredVersion } from "@ctrlplane/db/reconcilers";
4+
import * as schema from "@ctrlplane/db/schema";
5+
import { exitedStatus, JobStatus } from "@ctrlplane/validators/jobs";
6+
7+
interface ArgoWorkflowPayload {
8+
workflowName: string;
9+
namespace: string;
10+
uid: string;
11+
createdAt: string;
12+
startedAt: string;
13+
finishedAt: string | null;
14+
jobId: string | null;
15+
phase: string;
16+
eventType: string;
17+
}
18+
19+
const statusMap: Record<string, JobStatus> = {
20+
Succeeded: JobStatus.Successful,
21+
Failed: JobStatus.Failure,
22+
Running: JobStatus.InProgress,
23+
Pending: JobStatus.Pending,
24+
};
25+
26+
export const mapTriggerToStatus = (trigger: string): JobStatus | null =>
27+
statusMap[trigger] ?? null;
28+
29+
export const getJobId = (payload: ArgoWorkflowPayload) =>
30+
payload.jobId ?? payload.workflowName;
31+
32+
export const handleArgoWorkflow = async (payload: ArgoWorkflowPayload) => {
33+
const { uid, phase, startedAt, finishedAt } = payload;
34+
35+
const jobId = getJobId(payload);
36+
37+
const status = statusMap[phase] ?? null;
38+
if (status == null) return;
39+
40+
const isCompleted = exitedStatus.includes(status);
41+
const completedAt =
42+
isCompleted && finishedAt != null ? new Date(finishedAt) : null;
43+
44+
const [updated] = await db
45+
.update(schema.job)
46+
.set({
47+
externalId: uid,
48+
status,
49+
...(startedAt ? { startedAt: new Date(startedAt) } : {}),
50+
completedAt,
51+
updatedAt: new Date(),
52+
})
53+
.where(eq(schema.job.id, jobId))
54+
.returning();
55+
56+
if (updated == null) return;
57+
58+
const result = await db
59+
.select({ workspaceId: schema.deployment.workspaceId })
60+
.from(schema.releaseJob)
61+
.innerJoin(
62+
schema.release,
63+
eq(schema.releaseJob.releaseId, schema.release.id),
64+
)
65+
.innerJoin(
66+
schema.deployment,
67+
eq(schema.release.deploymentId, schema.deployment.id),
68+
)
69+
.where(eq(schema.releaseJob.jobId, jobId))
70+
.then((rows) => rows[0] ?? null);
71+
72+
if (result?.workspaceId == null) return;
73+
enqueueAllReleaseTargetsDesiredVersion(db, result.workspaceId);
74+
};

apps/api/src/server.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { auth } from "@ctrlplane/auth/server";
1616
import { appRouter, createTRPCContext } from "@ctrlplane/trpc";
1717

1818
import swaggerDocument from "../openapi/openapi.json" with { type: "json" };
19+
import { createArgoWorkflowRouter } from "./routes/argoworkflow/index.js";
1920
import { createGithubRouter } from "./routes/github/index.js";
2021
import { createTfeRouter } from "./routes/tfe/index.js";
2122

@@ -26,7 +27,7 @@ const specFile = join(__dirname, "../openapi/openapi.json");
2627
const oapiValidatorMiddleware = OpenApiValidator.middleware({
2728
apiSpec: specFile,
2829
validateRequests: true,
29-
ignorePaths: /\/api\/(auth|trpc|github|tfe|ui|healthz)/,
30+
ignorePaths: /\/api\/(auth|argo|trpc|github|tfe|ui|healthz)/,
3031
});
3132

3233
const trpcMiddleware = trpcExpress.createExpressMiddleware({
@@ -81,6 +82,7 @@ const app = express()
8182
.use("/api/v1", createV1Router())
8283
.use("/api/github", createGithubRouter())
8384
.use("/api/tfe", createTfeRouter())
85+
.use("/api/argo", createArgoWorkflowRouter())
8486
.use("/api/trpc", trpcMiddleware)
8587
.use(errorHandler);
8688

apps/workspace-engine/go.mod

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
module workspace-engine
22

3-
go 1.25.5
3+
go 1.25.7
44

55
require (
66
github.com/Masterminds/sprig/v3 v3.3.0
77
github.com/argoproj/argo-cd/v3 v3.3.4
8+
github.com/argoproj/argo-workflows/v4 v4.0.3
89
github.com/avast/retry-go v2.7.0+incompatible
910
github.com/charmbracelet/log v0.4.2
1011
github.com/confluentinc/confluent-kafka-go/v2 v2.13.3
@@ -34,6 +35,7 @@ require (
3435
go.opentelemetry.io/otel/sdk v1.41.0
3536
go.opentelemetry.io/otel/sdk/metric v1.41.0
3637
go.opentelemetry.io/otel/trace v1.41.0
38+
k8s.io/apimachinery v0.34.1
3739
sigs.k8s.io/yaml v1.6.0
3840
)
3941

@@ -96,9 +98,9 @@ require (
9698
go.uber.org/mock v0.6.0 // indirect
9799
golang.org/x/arch v0.20.0 // indirect
98100
golang.org/x/crypto v0.48.0 // indirect
99-
golang.org/x/mod v0.32.0 // indirect
101+
golang.org/x/mod v0.33.0 // indirect
100102
golang.org/x/sync v0.19.0
101-
golang.org/x/tools v0.41.0 // indirect
103+
golang.org/x/tools v0.42.0 // indirect
102104
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
103105
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
104106
google.golang.org/grpc v1.79.1 // indirect
@@ -109,6 +111,7 @@ require (
109111
cloud.google.com/go/compute/metadata v0.9.0 // indirect
110112
cyphar.com/go-pathrs v0.2.1 // indirect
111113
dario.cat/mergo v1.0.2 // indirect
114+
filippo.io/edwards25519 v1.1.1 // indirect
112115
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect
113116
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
114117
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
@@ -119,8 +122,9 @@ require (
119122
github.com/Masterminds/semver/v3 v3.4.0 // indirect
120123
github.com/Microsoft/go-winio v0.6.2 // indirect
121124
github.com/ProtonMail/go-crypto v1.3.0 // indirect
125+
github.com/argoproj/argo-events v1.9.6 // indirect
122126
github.com/argoproj/gitops-engine v0.7.1-0.20250908182407-97ad5b59a627 // indirect
123-
github.com/argoproj/pkg v0.13.7-0.20230626144333-d56162821bd1 // indirect
127+
github.com/argoproj/pkg v0.13.7-0.20250123033407-65f2d4777bfd // indirect
124128
github.com/argoproj/pkg/v2 v2.0.1 // indirect
125129
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
126130
github.com/beorn7/perks v1.0.1 // indirect
@@ -140,26 +144,30 @@ require (
140144
github.com/charmbracelet/x/term v0.2.1 // indirect
141145
github.com/clipperhouse/stringish v0.1.1 // indirect
142146
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
143-
github.com/cloudflare/circl v1.6.1 // indirect
147+
github.com/cloudflare/circl v1.6.3 // indirect
148+
github.com/colinmarc/hdfs/v2 v2.4.0 // indirect
144149
github.com/containerd/containerd/api v1.10.0 // indirect
145150
github.com/coreos/go-oidc/v3 v3.17.0 // indirect
146-
github.com/creack/pty v1.1.24 // indirect
147151
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
148152
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
149153
github.com/distribution/reference v0.6.0 // indirect
150154
github.com/dlclark/regexp2 v1.11.5 // indirect
155+
github.com/doublerebel/bellows v0.0.0-20160303004610-f177d92a03d3 // indirect
151156
github.com/dustin/go-humanize v1.0.1 // indirect
152157
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
153158
github.com/emirpasic/gods v1.18.1 // indirect
154159
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
160+
github.com/evilmonkeyinc/jsonpath v0.8.1 // indirect
155161
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
162+
github.com/expr-lang/expr v1.17.7 // indirect
156163
github.com/fatih/camelcase v1.0.0 // indirect
157164
github.com/felixge/httpsnoop v1.0.4 // indirect
158165
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
159166
github.com/go-errors/errors v1.5.1 // indirect
160167
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
161168
github.com/go-git/go-billy/v5 v5.6.2 // indirect
162-
github.com/go-git/go-git/v5 v5.14.0 // indirect
169+
github.com/go-git/go-git/v5 v5.16.5 // indirect
170+
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
163171
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
164172
github.com/go-logfmt/logfmt v0.6.0 // indirect
165173
github.com/go-openapi/swag/cmdutils v0.25.3 // indirect
@@ -174,10 +182,12 @@ require (
174182
github.com/go-openapi/swag/typeutils v0.25.3 // indirect
175183
github.com/go-openapi/swag/yamlutils v0.25.3 // indirect
176184
github.com/go-redis/cache/v9 v9.0.0 // indirect
177-
github.com/gobwas/glob v0.2.3 // indirect
185+
github.com/go-sql-driver/mysql v1.9.2 // indirect
186+
github.com/gobwas/glob v0.2.4-0.20181002190808-e7a84e9525fe // indirect
178187
github.com/gogo/protobuf v1.3.2 // indirect
179188
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
180189
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
190+
github.com/golang/mock v1.6.0 // indirect
181191
github.com/golang/protobuf v1.5.4 // indirect
182192
github.com/google/btree v1.1.3 // indirect
183193
github.com/google/gnostic-models v0.7.0 // indirect
@@ -192,21 +202,32 @@ require (
192202
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
193203
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
194204
github.com/hashicorp/go-slug v0.16.8 // indirect
205+
github.com/hashicorp/go-uuid v1.0.3 // indirect
195206
github.com/hashicorp/go-version v1.8.0 // indirect
196207
github.com/hashicorp/jsonapi v1.4.3-0.20250220162346-81a76b606f3e // indirect
197208
github.com/huandu/xstrings v1.5.0 // indirect
198209
github.com/inconshreveable/mousetrap v1.1.0 // indirect
210+
github.com/jackc/pgio v1.0.0 // indirect
211+
github.com/jackc/pgtype v1.14.4 // indirect
199212
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
213+
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
214+
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
215+
github.com/jcmturner/gofork v1.7.6 // indirect
216+
github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
217+
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
218+
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
200219
github.com/jonboulle/clockwork v0.5.0 // indirect
201220
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
202221
github.com/kevinburke/ssh_config v1.2.0 // indirect
203222
github.com/klauspost/compress v1.18.3 // indirect
223+
github.com/klauspost/pgzip v1.2.6 // indirect
204224
github.com/kylelemons/godebug v1.1.0 // indirect
225+
github.com/lib/pq v1.10.9 // indirect
205226
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
206227
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
207-
github.com/mattn/go-colorable v0.1.14 // indirect
208228
github.com/mattn/go-isatty v0.0.20 // indirect
209229
github.com/mattn/go-runewidth v0.0.19 // indirect
230+
github.com/mattn/go-sqlite3 v1.14.28 // indirect
210231
github.com/mitchellh/copystructure v1.2.0 // indirect
211232
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
212233
github.com/mitchellh/reflectwalk v1.0.2 // indirect
@@ -216,6 +237,7 @@ require (
216237
github.com/muesli/termenv v0.16.0 // indirect
217238
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
218239
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
240+
github.com/ncruces/go-strftime v0.1.9 // indirect
219241
github.com/opencontainers/go-digest v1.0.0 // indirect
220242
github.com/opencontainers/image-spec v1.1.1 // indirect
221243
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
@@ -228,16 +250,22 @@ require (
228250
github.com/prometheus/procfs v0.17.0 // indirect
229251
github.com/r3labs/diff/v3 v3.0.2 // indirect
230252
github.com/redis/go-redis/v9 v9.8.0 // indirect
253+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
231254
github.com/rivo/uniseg v0.4.7 // indirect
232255
github.com/robfig/cron/v3 v3.0.2-0.20210106135023-bc59245fe10e // indirect
233256
github.com/russross/blackfriday/v2 v2.1.0 // indirect
257+
github.com/segmentio/fasthash v1.0.3 // indirect
234258
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
259+
github.com/sethvargo/go-limiter v1.0.0 // indirect
235260
github.com/shopspring/decimal v1.4.0 // indirect
236261
github.com/sirupsen/logrus v1.9.4 // indirect
237262
github.com/skeema/knownhosts v1.3.1 // indirect
238-
github.com/spf13/cast v1.7.1 // indirect
263+
github.com/spf13/cast v1.9.2 // indirect
239264
github.com/spf13/pflag v1.0.10 // indirect
240265
github.com/testcontainers/testcontainers-go/modules/compose v0.41.0 // indirect
266+
github.com/upper/db/v4 v4.10.0 // indirect
267+
github.com/valyala/bytebufferpool v1.0.0 // indirect
268+
github.com/valyala/fasttemplate v1.2.2 // indirect
241269
github.com/vmihailenco/go-tinylfu v0.2.2 // indirect
242270
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
243271
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
@@ -246,24 +274,25 @@ require (
246274
github.com/xlab/treeprint v1.2.0 // indirect
247275
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
248276
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
277+
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect
278+
go.opentelemetry.io/otel/exporters/prometheus v0.58.0 // indirect
249279
go.yaml.in/yaml/v2 v2.4.3 // indirect
250280
go.yaml.in/yaml/v3 v3.0.4 // indirect
251-
golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect
281+
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
252282
golang.org/x/net v0.50.0 // indirect
253283
golang.org/x/oauth2 v0.35.0 // indirect
254284
golang.org/x/sys v0.41.0 // indirect
255285
golang.org/x/term v0.40.0 // indirect
256286
golang.org/x/text v0.34.0 // indirect
257287
golang.org/x/time v0.14.0 // indirect
258-
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
288+
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
259289
google.golang.org/protobuf v1.36.11 // indirect
260290
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
261291
gopkg.in/inf.v0 v0.9.1 // indirect
262292
gopkg.in/warnings.v0 v0.1.2 // indirect
263293
gopkg.in/yaml.v3 v3.0.1 // indirect
264294
k8s.io/api v0.34.1 // indirect
265295
k8s.io/apiextensions-apiserver v0.34.0 // indirect
266-
k8s.io/apimachinery v0.34.1 // indirect
267296
k8s.io/apiserver v0.34.0 // indirect
268297
k8s.io/cli-runtime v0.34.0 // indirect
269298
k8s.io/client-go v0.34.1 // indirect
@@ -275,13 +304,18 @@ require (
275304
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
276305
k8s.io/kubectl v0.34.0 // indirect
277306
k8s.io/kubernetes v1.34.2 // indirect
278-
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
307+
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
308+
modernc.org/libc v1.65.8 // indirect
309+
modernc.org/mathutil v1.7.1 // indirect
310+
modernc.org/memory v1.11.0 // indirect
311+
modernc.org/sqlite v1.37.1 // indirect
279312
oras.land/oras-go/v2 v2.6.0 // indirect
280-
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
313+
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
281314
sigs.k8s.io/kustomize/api v0.20.1 // indirect
282315
sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect
283316
sigs.k8s.io/randfill v1.0.0 // indirect
284317
sigs.k8s.io/structured-merge-diff/v6 v6.3.1-0.20251003215857-446d8398e19c // indirect
318+
zombiezen.com/go/sqlite v1.4.2 // indirect
285319
)
286320

287321
tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen

0 commit comments

Comments
 (0)