Skip to content

Commit e92cea2

Browse files
refactor: Introduce deployment version endpoints in api v1 (#442)
1 parent 8f1c526 commit e92cea2

File tree

21 files changed

+1265
-16
lines changed

21 files changed

+1265
-16
lines changed

Diff for: apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/environments/[environmentId]/deployments/EnvironmentDeploymentsPageContent.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client";
22

3+
import type { JobCondition } from "@ctrlplane/validators/jobs";
34
import React, { useState } from "react";
45
import { useParams, useRouter } from "next/navigation";
56
import { IconSearch } from "@tabler/icons-react";
@@ -26,7 +27,7 @@ import {
2627
TableRow,
2728
} from "@ctrlplane/ui/table";
2829
import { ColumnOperator } from "@ctrlplane/validators/conditions";
29-
import { JobCondition, JobConditionType } from "@ctrlplane/validators/jobs";
30+
import { JobConditionType } from "@ctrlplane/validators/jobs";
3031

3132
import { urls } from "~/app/urls";
3233
import { api } from "~/trpc/react";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import type { Swagger } from "atlassian-openapi";
2+
3+
export const openapi: Swagger.SwaggerV3 = {
4+
openapi: "3.0.0",
5+
info: {
6+
title: "Ctrlplane API",
7+
version: "1.0.0",
8+
},
9+
paths: {
10+
"/v1/deployment-version-channels": {
11+
post: {
12+
summary: "Create a deployment version channel",
13+
operationId: "createDeploymentVersionChannel",
14+
requestBody: {
15+
required: true,
16+
content: {
17+
"application/json": {
18+
schema: {
19+
type: "object",
20+
required: ["deploymentId", "name", "versionSelector"],
21+
properties: {
22+
deploymentId: { type: "string" },
23+
name: { type: "string" },
24+
description: { type: "string", nullable: true },
25+
versionSelector: {
26+
type: "object",
27+
additionalProperties: true,
28+
},
29+
},
30+
},
31+
},
32+
},
33+
},
34+
responses: {
35+
"200": {
36+
description: "Deployment version channel created successfully",
37+
content: {
38+
"application/json": {
39+
schema: {
40+
type: "object",
41+
properties: {
42+
id: { type: "string" },
43+
deploymentId: { type: "string" },
44+
name: { type: "string" },
45+
description: { type: "string", nullable: true },
46+
createdAt: { type: "string", format: "date-time" },
47+
versionSelector: {
48+
type: "object",
49+
additionalProperties: true,
50+
},
51+
},
52+
required: ["id", "deploymentId", "name", "createdAt"],
53+
},
54+
},
55+
},
56+
},
57+
"409": {
58+
description: "Deployment version channel already exists",
59+
content: {
60+
"application/json": {
61+
schema: {
62+
type: "object",
63+
properties: {
64+
error: { type: "string" },
65+
id: { type: "string" },
66+
},
67+
required: ["error", "id"],
68+
},
69+
},
70+
},
71+
},
72+
"500": {
73+
description: "Failed to create deployment version channel",
74+
content: {
75+
"application/json": {
76+
schema: {
77+
type: "object",
78+
properties: { error: { type: "string" } },
79+
required: ["error"],
80+
},
81+
},
82+
},
83+
},
84+
"401": {
85+
description: "Unauthorized",
86+
content: {
87+
"application/json": {
88+
schema: {
89+
type: "object",
90+
properties: { error: { type: "string" } },
91+
required: ["error"],
92+
},
93+
},
94+
},
95+
},
96+
"403": {
97+
description: "Forbidden",
98+
content: {
99+
"application/json": {
100+
schema: {
101+
type: "object",
102+
properties: { error: { type: "string" } },
103+
required: ["error"],
104+
},
105+
},
106+
},
107+
},
108+
},
109+
security: [{ bearerAuth: [] }],
110+
},
111+
},
112+
},
113+
components: {
114+
securitySchemes: { bearerAuth: { type: "http", scheme: "bearer" } },
115+
},
116+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import type { z } from "zod";
2+
import { NextResponse } from "next/server";
3+
4+
import { buildConflictUpdateColumns, takeFirst } from "@ctrlplane/db";
5+
import { createDeploymentVersionChannel } from "@ctrlplane/db/schema";
6+
import * as SCHEMA from "@ctrlplane/db/schema";
7+
import { logger } from "@ctrlplane/logger";
8+
import { Permission } from "@ctrlplane/validators/auth";
9+
10+
import { authn, authz } from "../auth";
11+
import { parseBody } from "../body-parser";
12+
import { request } from "../middleware";
13+
14+
const schema = createDeploymentVersionChannel;
15+
16+
export const POST = request()
17+
.use(authn)
18+
.use(parseBody(schema))
19+
.use(
20+
authz(({ ctx, can }) =>
21+
can
22+
.perform(Permission.DeploymentVersionChannelCreate)
23+
.on({ type: "deployment", id: ctx.body.deploymentId }),
24+
),
25+
)
26+
.handle<{ body: z.infer<typeof schema> }>(({ db, body }) => {
27+
const { versionSelector } = body;
28+
29+
return db
30+
.insert(SCHEMA.deploymentVersionChannel)
31+
.values({ ...body, versionSelector })
32+
.onConflictDoUpdate({
33+
target: [
34+
SCHEMA.deploymentVersionChannel.deploymentId,
35+
SCHEMA.deploymentVersionChannel.name,
36+
],
37+
set: buildConflictUpdateColumns(SCHEMA.deploymentVersionChannel, [
38+
"versionSelector",
39+
]),
40+
})
41+
.returning()
42+
.then(takeFirst)
43+
.then((deploymentVersionChannel) =>
44+
NextResponse.json(deploymentVersionChannel),
45+
)
46+
.catch((error) => {
47+
logger.error(error);
48+
return NextResponse.json(
49+
{ error: "Failed to create deployment version channel" },
50+
{ status: 500 },
51+
);
52+
});
53+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import type { Swagger } from "atlassian-openapi";
2+
3+
import { DeploymentVersionStatus } from "@ctrlplane/validators/releases";
4+
5+
export const openapi: Swagger.SwaggerV3 = {
6+
openapi: "3.0.0",
7+
info: { title: "Ctrlplane API", version: "1.0.0" },
8+
paths: {
9+
"/v1/deployment-versions/{deploymentVersionId}": {
10+
patch: {
11+
summary: "Updates a deployment version",
12+
operationId: "updateDeploymentVersion",
13+
parameters: [
14+
{
15+
name: "deploymentVersionId",
16+
in: "path",
17+
required: true,
18+
schema: { type: "string" },
19+
description: "The deployment version ID",
20+
},
21+
],
22+
requestBody: {
23+
required: true,
24+
content: {
25+
"application/json": {
26+
schema: {
27+
type: "object",
28+
properties: {
29+
tag: { type: "string" },
30+
deploymentId: { type: "string" },
31+
createdAt: { type: "string", format: "date-time" },
32+
name: { type: "string" },
33+
config: { type: "object", additionalProperties: true },
34+
jobAgentConfig: {
35+
type: "object",
36+
additionalProperties: true,
37+
},
38+
status: {
39+
type: "string",
40+
enum: Object.values(DeploymentVersionStatus),
41+
},
42+
message: { type: "string" },
43+
metadata: {
44+
type: "object",
45+
additionalProperties: { type: "string" },
46+
},
47+
},
48+
},
49+
},
50+
},
51+
},
52+
responses: {
53+
"200": {
54+
description: "OK",
55+
content: {
56+
"application/json": {
57+
schema: { $ref: "#/components/schemas/DeploymentVersion" },
58+
},
59+
},
60+
},
61+
},
62+
},
63+
},
64+
},
65+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { NextResponse } from "next/server";
2+
import httpStatus from "http-status";
3+
import { z } from "zod";
4+
5+
import { buildConflictUpdateColumns, eq, takeFirst } from "@ctrlplane/db";
6+
import * as SCHEMA from "@ctrlplane/db/schema";
7+
import {
8+
cancelOldReleaseJobTriggersOnJobDispatch,
9+
createJobApprovals,
10+
createReleaseJobTriggers,
11+
dispatchReleaseJobTriggers,
12+
isPassingAllPolicies,
13+
isPassingChannelSelectorPolicy,
14+
} from "@ctrlplane/job-dispatch";
15+
import { logger } from "@ctrlplane/logger";
16+
import { Permission } from "@ctrlplane/validators/auth";
17+
18+
import { authn, authz } from "../../auth";
19+
import { parseBody } from "../../body-parser";
20+
import { request } from "../../middleware";
21+
22+
const patchSchema = SCHEMA.updateDeploymentVersion.and(
23+
z.object({ metadata: z.record(z.string()).optional() }),
24+
);
25+
26+
export const PATCH = request()
27+
.use(authn)
28+
.use(parseBody(patchSchema))
29+
.use(
30+
authz(({ can, extra: { params } }) =>
31+
can
32+
.perform(Permission.DeploymentVersionUpdate)
33+
.on({ type: "deploymentVersion", id: params.deploymentVersionId }),
34+
),
35+
)
36+
.handle<
37+
{ body: z.infer<typeof patchSchema>; user: SCHEMA.User },
38+
{ params: { deploymentVersionId: string } }
39+
>(async (ctx, { params }) => {
40+
const { deploymentVersionId } = params;
41+
const { body, user, req } = ctx;
42+
43+
try {
44+
const deploymentVersion = await ctx.db
45+
.update(SCHEMA.deploymentVersion)
46+
.set(body)
47+
.where(eq(SCHEMA.deploymentVersion.id, deploymentVersionId))
48+
.returning()
49+
.then(takeFirst);
50+
51+
if (Object.keys(body.metadata ?? {}).length > 0)
52+
await ctx.db
53+
.insert(SCHEMA.deploymentVersionMetadata)
54+
.values(
55+
Object.entries(body.metadata ?? {}).map(([key, value]) => ({
56+
versionId: deploymentVersionId,
57+
key,
58+
value,
59+
})),
60+
)
61+
.onConflictDoUpdate({
62+
target: [
63+
SCHEMA.deploymentVersionMetadata.key,
64+
SCHEMA.deploymentVersionMetadata.versionId,
65+
],
66+
set: buildConflictUpdateColumns(SCHEMA.deploymentVersionMetadata, [
67+
"value",
68+
]),
69+
});
70+
71+
await createReleaseJobTriggers(ctx.db, "version_updated")
72+
.causedById(user.id)
73+
.filter(isPassingChannelSelectorPolicy)
74+
.versions([deploymentVersionId])
75+
.then(createJobApprovals)
76+
.insert()
77+
.then((releaseJobTriggers) => {
78+
dispatchReleaseJobTriggers(ctx.db)
79+
.releaseTriggers(releaseJobTriggers)
80+
.filter(isPassingAllPolicies)
81+
.then(cancelOldReleaseJobTriggersOnJobDispatch)
82+
.dispatch();
83+
})
84+
.then(() =>
85+
logger.info(
86+
`Version for ${deploymentVersionId} job triggers created and dispatched.`,
87+
req,
88+
),
89+
);
90+
91+
return NextResponse.json(deploymentVersion);
92+
} catch (error) {
93+
logger.error(error);
94+
return NextResponse.json(
95+
{ error: "Failed to update version" },
96+
{ status: httpStatus.INTERNAL_SERVER_ERROR },
97+
);
98+
}
99+
});

0 commit comments

Comments
 (0)