Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type { Swagger } from "atlassian-openapi";

export const openapi: Swagger.SwaggerV3 = {
openapi: "3.0.0",
info: {
title: "Ctrlplane API",
version: "1.0.0",
},
paths: {
"/v1/release-targets/{releaseTargetId}/pin": {
post: {
summary: "Pin a version to a release target",
operationId: "pinReleaseTarget",
parameters: [
{
name: "releaseTargetId",
in: "path",
required: true,
schema: { type: "string", format: "uuid" },
},
],
requestBody: {
required: true,
content: {
"application/json": {
schema: {
oneOf: [
{
type: "object",
properties: {
versionId: {
type: "string",
format: "uuid",
example: "123e4567-e89b-12d3-a456-426614174000",
description: "The ID of the version to pin",
},
},
required: ["versionId"],
},
{
type: "object",
properties: {
versionTag: {
type: "string",
example: "1.0.0",
description: "The tag of the version to pin",
},
},
required: ["versionTag"],
},
],
},
},
},
},
responses: {
200: {
description: "Version pinned",
content: {
"application/json": {
schema: {
type: "object",
properties: { success: { type: "boolean" } },
},
},
},
},
400: {
description: "Bad request",
content: {
"application/json": {
schema: {
type: "object",
properties: { error: { type: "string" } },
},
},
},
},
404: {
description: "Version not found",
content: {
"application/json": {
schema: {
type: "object",
properties: { error: { type: "string" } },
},
},
},
},
500: {
description: "Internal server error",
content: {
"application/json": {
schema: {
type: "object",
properties: { error: { type: "string" } },
},
},
},
},
},
},
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import type { Tx } from "@ctrlplane/db";
import { NextResponse } from "next/server";
import { INTERNAL_SERVER_ERROR, NOT_FOUND } from "http-status";
import { z } from "zod";

import { and, eq, takeFirstOrNull } from "@ctrlplane/db";
import * as schema from "@ctrlplane/db/schema";
import { dispatchQueueJob } from "@ctrlplane/events";
import { logger } from "@ctrlplane/logger";
import { Permission } from "@ctrlplane/validators/auth";

import { authn, authz } from "~/app/api/v1/auth";
import { parseBody } from "~/app/api/v1/body-parser";
import { request } from "~/app/api/v1/middleware";

const log = logger.child({
path: "/api/v1/release-targets/[releaseTargetId]/pin",
});

const bodySchema = z.union([
z.object({ versionId: z.string().uuid() }),
z.object({ versionTag: z.string() }),
]);

const getVersion = async (
db: Tx,
body: z.infer<typeof bodySchema>,
releaseTarget: schema.ReleaseTarget,
) => {
if ("versionId" in body)
return db
.select()
.from(schema.deploymentVersion)
.where(eq(schema.deploymentVersion.id, body.versionId))
.then(takeFirstOrNull);

return db
.select()
.from(schema.deploymentVersion)
.where(
and(
eq(schema.deploymentVersion.deploymentId, releaseTarget.deploymentId),
eq(schema.deploymentVersion.tag, body.versionTag),
),
)
.then(takeFirstOrNull);
};

const getReleaseTarget = async (db: Tx, releaseTargetId: string) =>
db
.select()
.from(schema.releaseTarget)
.where(eq(schema.releaseTarget.id, releaseTargetId))
.then(takeFirstOrNull);

const pinVersion = async (db: Tx, releaseTargetId: string, versionId: string) =>
db
.update(schema.releaseTarget)
.set({ desiredVersionId: versionId })
.where(eq(schema.releaseTarget.id, releaseTargetId));

// const unpinVersion = async (db: Tx, releaseTarget: schema.ReleaseTarget) => {
// if (releaseTarget.desiredVersionId == null)
// return NextResponse.json(
// { error: "No version pinned" },
// { status: BAD_REQUEST },
// );

// await db
// .update(schema.releaseTarget)
// .set({ desiredVersionId: null })
// .where(eq(schema.releaseTarget.id, releaseTarget.id));

// await dispatchQueueJob().toEvaluate().releaseTargets([releaseTarget]);

// return NextResponse.json({ success: true });
// };

export const POST = request()
.use(authn)
.use(
authz(({ can, params }) =>
can.perform(Permission.ReleaseTargetGet).on({
type: "releaseTarget",
id: params.releaseTargetId ?? "",
}),
),
)
.use(parseBody(bodySchema))
.handle<
{ db: Tx; body: z.infer<typeof bodySchema> },
{ params: Promise<{ releaseTargetId: string }> }
>(async ({ db, body }, { params }) => {
try {
const { releaseTargetId } = await params;

const releaseTarget = await getReleaseTarget(db, releaseTargetId);
if (releaseTarget == null)
return NextResponse.json(
{ error: "Release target not found" },
{ status: NOT_FOUND },
);

const version = await getVersion(db, body, releaseTarget);
if (version == null)
return NextResponse.json(
{ error: "Version not found" },
{ status: NOT_FOUND },
);

await pinVersion(db, releaseTargetId, version.id);
await dispatchQueueJob().toEvaluate().releaseTargets([releaseTarget]);

return NextResponse.json({ success: true });
} catch (error) {
log.error("Failed to pin version", { error });
return NextResponse.json(
{ error: "Failed to pin version" },
{ status: INTERNAL_SERVER_ERROR },
);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { Swagger } from "atlassian-openapi";

export const openapi: Swagger.SwaggerV3 = {
openapi: "3.0.0",
info: {
title: "Ctrlplane API",
version: "1.0.0",
},
paths: {
"/v1/release-targets/{releaseTargetId}/unpin": {
post: {
summary: "Unpin a version from a release target",
operationId: "unpinReleaseTarget",
parameters: [
{
name: "releaseTargetId",
in: "path",
required: true,
schema: { type: "string", format: "uuid" },
},
],
responses: {
200: {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: { success: { type: "boolean" } },
},
},
},
},
400: {
description: "Bad Request",
content: {
"application/json": {
schema: {
type: "object",
properties: { error: { type: "string" } },
},
},
},
},
404: {
description: "Not Found",
content: {
"application/json": {
schema: {
type: "object",
properties: { error: { type: "string" } },
},
},
},
},
500: {
description: "Internal Server Error",
content: {
"application/json": {
schema: {
type: "object",
properties: { error: { type: "string" } },
},
},
},
},
},
},
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import type { Tx } from "@ctrlplane/db";
import { NextResponse } from "next/server";
import { BAD_REQUEST, INTERNAL_SERVER_ERROR, NOT_FOUND } from "http-status";

import { eq, takeFirstOrNull } from "@ctrlplane/db";
import * as schema from "@ctrlplane/db/schema";
import { dispatchQueueJob } from "@ctrlplane/events";
import { logger } from "@ctrlplane/logger";
import { Permission } from "@ctrlplane/validators/auth";

import { authn, authz } from "~/app/api/v1/auth";
import { request } from "~/app/api/v1/middleware";

const log = logger.child({
path: "/api/v1/release-targets/[releaseTargetId]/unpin",
});

const getReleaseTarget = async (db: Tx, releaseTargetId: string) =>
db
.select()
.from(schema.releaseTarget)
.where(eq(schema.releaseTarget.id, releaseTargetId))
.then(takeFirstOrNull);

const getPinnedVersion = async (
db: Tx,
releaseTarget: schema.ReleaseTarget,
) => {
const { desiredVersionId } = releaseTarget;
if (desiredVersionId == null) return null;

return db
.select()
.from(schema.deploymentVersion)
.where(eq(schema.deploymentVersion.id, desiredVersionId))
.then(takeFirstOrNull);
};

const unpinVersion = async (db: Tx, releaseTargetId: string) =>
db
.update(schema.releaseTarget)
.set({ desiredVersionId: null })
.where(eq(schema.releaseTarget.id, releaseTargetId));

export const POST = request()
.use(authn)
.use(
authz(({ can, params }) =>
can.perform(Permission.ReleaseTargetGet).on({
type: "releaseTarget",
id: params.releaseTargetId ?? "",
}),
),
)
.handle<{ db: Tx }, { params: Promise<{ releaseTargetId: string }> }>(
async ({ db }, { params }) => {
try {
const { releaseTargetId } = await params;

const releaseTarget = await getReleaseTarget(db, releaseTargetId);
if (releaseTarget == null)
return NextResponse.json(
{ error: "Release target not found" },
{ status: NOT_FOUND },
);

const pinnedVersion = await getPinnedVersion(db, releaseTarget);
if (pinnedVersion == null)
return NextResponse.json(
{ error: "No version pinned" },
{ status: BAD_REQUEST },
);

await unpinVersion(db, releaseTargetId);
await dispatchQueueJob().toEvaluate().releaseTargets([releaseTarget]);

return NextResponse.json({ success: true });
} catch (error) {
log.error("Failed to unpin version", { error });
return NextResponse.json(
{ error: "Failed to unpin version" },
{ status: INTERNAL_SERVER_ERROR },
);
}
},
);
Loading
Loading