Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/api/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const env = createEnv({
GITHUB_BOT_PRIVATE_KEY: z.string().optional(),
GITHUB_WEBHOOK_SECRET: z.string().optional(),
TFE_WEBHOOK_SECRET: z.string().optional(),
ARGO_WORKFLOW_WEBHOOK_SECRET: z.string().optional(),

BASE_URL: z.string().optional(),

Expand Down
29 changes: 29 additions & 0 deletions apps/api/src/routes/argoworkflow/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Request, Response } from "express";
import { env } from "@/config.js";
import { asyncHandler } from "@/types/api.js";
import { Router } from "express";

import { handleArgoWorkflow } from "./run_workflow.js";

export const createArgoWorkflowRouter = (): Router =>
Router().post("/webhook", asyncHandler(handleWebhookRequest));

const verifyRequest = async (req: Request): Promise<boolean> => {

Check failure on line 11 in apps/api/src/routes/argoworkflow/index.ts

View workflow job for this annotation

GitHub Actions / Lint

Async arrow function 'verifyRequest' has no 'await' expression
const authHeader = req.headers["authorization"]?.toString();

Check failure on line 12 in apps/api/src/routes/argoworkflow/index.ts

View workflow job for this annotation

GitHub Actions / Lint

["authorization"] is better written in dot notation
if (authHeader == null) return false;
const secret = env.ARGO_WORKFLOW_WEBHOOK_SECRET;
return authHeader === secret;
};

const handleWebhookRequest = async (req: Request, res: Response) => {
const isVerified = await verifyRequest(req);
if (!isVerified) {
res.status(401).json({ message: "Unauthorized" });
return;
}

const payload = req.body;
console.log("handleArgoWorkflow payload:", JSON.stringify(payload, null, 2));
await handleArgoWorkflow(payload);
res.status(200).send();
};
78 changes: 78 additions & 0 deletions apps/api/src/routes/argoworkflow/run_workflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { eq } from "@ctrlplane/db";
import { db } from "@ctrlplane/db/client";
import { enqueueAllReleaseTargetsDesiredVersion } from "@ctrlplane/db/reconcilers";
import * as schema from "@ctrlplane/db/schema";
import { exitedStatus, JobStatus } from "@ctrlplane/validators/jobs";

interface ArgoWorkflowPayload {
workflowName: string;
namespace: string;
uid: string;
createdAt: string;
startedAt: string;
finishedAt: string | null;
phase: string;
eventType: string;
}

const statusMap: Record<string, JobStatus> = {
Succeeded: JobStatus.Successful,
Failed: JobStatus.Failure,
Running: JobStatus.InProgress,
Pending: JobStatus.Pending,
};

const extractUuid = (str: string) => {

Check failure on line 25 in apps/api/src/routes/argoworkflow/run_workflow.ts

View workflow job for this annotation

GitHub Actions / Lint

'extractUuid' is assigned a value but never used. Allowed unused vars must match /^_/u
const uuidRegex =
/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/;
const match = uuidRegex.exec(str);
return match ? match[0] : null;
};

export const mapTriggerToStatus = (trigger: string): JobStatus | null =>
statusMap[trigger] ?? null;

export const handleArgoWorkflow = async (payload: ArgoWorkflowPayload) => {
const { workflowName, uid, phase, startedAt, finishedAt } = payload;

Check failure on line 36 in apps/api/src/routes/argoworkflow/run_workflow.ts

View workflow job for this annotation

GitHub Actions / Lint

'workflowName' is assigned a value but never used. Allowed unused vars must match /^_/u

const jobId = uid;
if (jobId == null) return;

Check failure on line 39 in apps/api/src/routes/argoworkflow/run_workflow.ts

View workflow job for this annotation

GitHub Actions / Lint

Unnecessary conditional, the types have no overlap

const status = statusMap[phase] ?? null;
if (status == null) return;

const isCompleted = exitedStatus.includes(status);
const completedAt =
isCompleted && finishedAt != null ? new Date(finishedAt) : null;

const [updated] = await db
.update(schema.job)
.set({
externalId: uid,
status,
startedAt: new Date(startedAt),
completedAt,
updatedAt: new Date(),
})
.where(eq(schema.job.id, jobId))
.returning();

if (updated == null) return;

const result = await db
.select({ workspaceId: schema.deployment.workspaceId })
.from(schema.releaseJob)
.innerJoin(
schema.release,
eq(schema.releaseJob.releaseId, schema.release.id),
)
.innerJoin(
schema.deployment,
eq(schema.release.deploymentId, schema.deployment.id),
)
.where(eq(schema.releaseJob.jobId, jobId))
.then((rows) => rows[0] ?? null);

if (result?.workspaceId == null) return;
enqueueAllReleaseTargetsDesiredVersion(db, result.workspaceId);
};
7 changes: 6 additions & 1 deletion apps/api/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { auth } from "@ctrlplane/auth/server";
import { appRouter, createTRPCContext } from "@ctrlplane/trpc";

import swaggerDocument from "../openapi/openapi.json" with { type: "json" };
import { createArgoWorkflowRouter } from "./routes/argoworkflow";
import { createGithubRouter } from "./routes/github/index.js";
import { createTfeRouter } from "./routes/tfe/index.js";

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

const trpcMiddleware = trpcExpress.createExpressMiddleware({
Expand Down Expand Up @@ -80,7 +81,11 @@ const app = express()
.use("/api/v1", requireAuth)
.use("/api/v1", createV1Router())
.use("/api/github", createGithubRouter())
<<<<<<< Updated upstream
.use("/api/tfe", createTfeRouter())
=======
.use("/api/argo", createArgoWorkflowRouter())
>>>>>>> Stashed changes
.use("/api/trpc", trpcMiddleware)
.use(errorHandler);

Expand Down
22 changes: 22 additions & 0 deletions apps/workspace-engine/oapi/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,28 @@
],
"type": "object"
},
"ArgoWorkflowJobAgentConfig": {
"properties": {
"apiKey": {
"description": "ArgoWorkflow API token.",
"type": "string"
},
"serverUrl": {
"description": "ArgoWorkflow server address (host[:port] or URL).",
"type": "string"
},
"uid": {
"description": "ArgoWorkflow job id ",
"type": "string"
}
},
"required": [
"uid",
"serverUrl",
"apiKey"
],
"type": "object"
},
"BasicResource": {
"properties": {
"id": {
Expand Down
10 changes: 10 additions & 0 deletions apps/workspace-engine/oapi/spec/schemas/jobs.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,16 @@ local JobPropertyKeys = std.objectFields(Job.properties);
},
},

ArgoWorkflowJobAgentConfig: {
type: 'object',
required: ['uid', 'serverUrl', 'apiKey'],
properties: {
serverUrl: { type: 'string', description: 'ArgoWorkflow server address (host[:port] or URL).' },
uid: { type: 'string', description: 'ArgoWorkflow job id ' },
apiKey: { type: 'string', description: 'ArgoWorkflow API token.' },
},
},

TestRunnerJobAgentConfig: {
type: 'object',
properties: {
Expand Down
12 changes: 12 additions & 0 deletions apps/workspace-engine/pkg/oapi/oapi.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading