Skip to content

Commit

Permalink
fix: Ephemeral envs init (#187)
Browse files Browse the repository at this point in the history
  • Loading branch information
adityachoudhari26 authored Oct 31, 2024
1 parent e16bb37 commit 4a0a13e
Show file tree
Hide file tree
Showing 28 changed files with 5,211 additions and 101 deletions.
3 changes: 3 additions & 0 deletions apps/jobs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@
"@ctrlplane/logger": "workspace:*",
"@ctrlplane/validators": "workspace:*",
"cron": "^3.1.7",
"lodash": "^4.17.21",
"ts-is-present": "^1.2.2",
"zod": "catalog:"
},
"devDependencies": {
"@ctrlplane/eslint-config": "workspace:^",
"@ctrlplane/prettier-config": "workspace:^",
"@ctrlplane/tsconfig": "workspace:*",
"@types/lodash": "^4.17.5",
"eslint": "catalog:",
"prettier": "catalog:",
"tsx": "catalog:",
Expand Down
56 changes: 56 additions & 0 deletions apps/jobs/src/expired-env-checker/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import _ from "lodash";
import { isPresent } from "ts-is-present";

import { eq, inArray, lte } from "@ctrlplane/db";
import { db } from "@ctrlplane/db/client";
import * as SCHEMA from "@ctrlplane/db/schema";
import { logger } from "@ctrlplane/logger";

type QueryRow = {
environment: SCHEMA.Environment;
deployment: SCHEMA.Deployment;
};

const groupByEnvironment = (rows: QueryRow[]) =>
_.chain(rows)
.groupBy((e) => e.environment.id)
.map((env) => ({
...env[0]!.environment,
deployments: env.map((e) => e.deployment),
}))
.value();

export const run = async () => {
const expiredEnvironments = await db
.select()
.from(SCHEMA.environment)
.innerJoin(
SCHEMA.deployment,
eq(SCHEMA.deployment.systemId, SCHEMA.environment.systemId),
)
.where(lte(SCHEMA.environment.expiresAt, new Date()))
.then(groupByEnvironment);
if (expiredEnvironments.length === 0) return;

const targetPromises = expiredEnvironments
.filter((env) => isPresent(env.targetFilter))
.map(async (env) => {
const targets = await db
.select()
.from(SCHEMA.target)
.where(SCHEMA.targetMatchesMetadata(db, env.targetFilter));

return { environmentId: env.id, targets };
});
const associatedTargets = await Promise.all(targetPromises);

for (const { environmentId, targets } of associatedTargets)
logger.info(
`[${targets.length}] targets are associated with expired environment [${environmentId}]`,
);

const envIds = expiredEnvironments.map((env) => env.id);
await db
.delete(SCHEMA.environment)
.where(inArray(SCHEMA.environment.id, envIds));
};
5 changes: 5 additions & 0 deletions apps/jobs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@ import { z } from "zod";

import { logger } from "@ctrlplane/logger";

import { run as expiredEnvChecker } from "./expired-env-checker/index.js";
import { run as jobPolicyChecker } from "./policy-checker/index.js";

const jobs: Record<string, { run: () => Promise<void>; schedule: string }> = {
"policy-checker": {
run: jobPolicyChecker,
schedule: "* * * * *", // Default: Every minute
},
"expired-env-checker": {
run: expiredEnvChecker,
schedule: "* * * * *", // Default: Every minute
},
};

const jobSchema = z.object({
Expand Down
3 changes: 2 additions & 1 deletion apps/jobs/src/policy-checker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
dispatchReleaseJobTriggers,
isPassingAllPolicies,
} from "@ctrlplane/job-dispatch";
import { logger } from "@ctrlplane/logger";
import { JobStatus } from "@ctrlplane/validators/jobs";

export const run = async () => {
Expand Down Expand Up @@ -44,7 +45,7 @@ export const run = async () => {
.then((rows) => rows.map((row) => row.release_job_trigger));

if (releaseJobTriggers.length === 0) return;
console.log(
logger.info(
`Found [${releaseJobTriggers.length}] release job triggers to dispatch`,
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const EnvironmentDrawer: React.FC = () => {
const environmentQ = api.environment.byId.useQuery(environmentId ?? "", {
enabled: isOpen,
});
const environmentQError = environmentQ.error;
const environment = environmentQ.data;

const { workspaceSlug } = useParams<{ workspaceSlug: string }>();
Expand All @@ -75,17 +76,24 @@ export const EnvironmentDrawer: React.FC = () => {
showBar={false}
className="left-auto right-0 top-0 mt-0 h-screen w-2/3 max-w-6xl overflow-auto rounded-none focus-visible:outline-none"
>
<DrawerTitle className="flex items-center gap-2 border-b p-6">
<div className="flex-shrink-0 rounded bg-green-500/20 p-1 text-green-400">
<IconPlant className="h-4 w-4" />
<DrawerTitle className="flex flex-col gap-2 border-b p-6">
<div className="flex items-center gap-2">
<div className="flex-shrink-0 rounded bg-green-500/20 p-1 text-green-400">
<IconPlant className="h-4 w-4" />
</div>
{environment?.name}
{environment != null && (
<EnvironmentDropdownMenu environment={environment}>
<Button variant="ghost" size="icon" className="h-6 w-6">
<IconDotsVertical className="h-4 w-4" />
</Button>
</EnvironmentDropdownMenu>
)}
</div>
{environment?.name}
{environment != null && (
<EnvironmentDropdownMenu environment={environment}>
<Button variant="ghost" size="icon" className="h-6 w-6">
<IconDotsVertical className="h-4 w-4" />
</Button>
</EnvironmentDropdownMenu>
{environmentQError != null && (
<div className="text-xs text-red-500">
{environmentQError.message}
</div>
)}
</DrawerTitle>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import type * as SCHEMA from "@ctrlplane/db/schema";
import { IconX } from "@tabler/icons-react";
import { z } from "zod";

import { Button } from "@ctrlplane/ui/button";
import { DateTimePicker } from "@ctrlplane/ui/datetime-picker";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
useForm,
} from "@ctrlplane/ui/form";
import { Input } from "@ctrlplane/ui/input";
Expand All @@ -18,14 +21,31 @@ import { api } from "~/trpc/react";
const schema = z.object({
name: z.string().min(1).max(100),
description: z.string().max(1000).nullable(),
expiresAt: z
.date()
.min(new Date(), "Expires at must be in the future")
.optional(),
});

type OverviewProps = {
environment: SCHEMA.Environment;
};

const isUsing12HourClock = (): boolean => {
const date = new Date();
const options: Intl.DateTimeFormatOptions = {
hour: "numeric",
};
const formattedTime = new Intl.DateTimeFormat(undefined, options).format(
date,
);
return formattedTime.includes("AM") || formattedTime.includes("PM");
};

export const Overview: React.FC<OverviewProps> = ({ environment }) => {
const form = useForm({ schema, defaultValues: environment });
const expiresAt = environment.expiresAt ?? undefined;
const defaultValues = { ...environment, expiresAt };
const form = useForm({ schema, defaultValues });
const update = api.environment.update.useMutation();
const envOverride = api.job.trigger.create.byEnvId.useMutation();

Expand Down Expand Up @@ -71,6 +91,35 @@ export const Overview: React.FC<OverviewProps> = ({ environment }) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="expiresAt"
render={({ field: { value, onChange } }) => (
<FormItem>
<FormLabel>Expires at</FormLabel>
<FormControl>
<div className="flex items-center gap-2">
<DateTimePicker
value={value}
onChange={onChange}
granularity="minute"
hourCycle={isUsing12HourClock() ? 12 : 24}
className="w-60"
/>
<Button
variant="ghost"
size="icon"
type="button"
onClick={() => onChange(undefined)}
>
<IconX className="h-4 w-4" />
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<div className="flex items-center gap-2">
<Button
Expand Down
35 changes: 26 additions & 9 deletions apps/webservice/src/app/api/v1/environments/route.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import type { PermissionChecker } from "@ctrlplane/auth/utils";
import type { User } from "@ctrlplane/db/schema";
import type { z } from "zod";
import { NextResponse } from "next/server";
import { isPresent } from "ts-is-present";
import { z } from "zod";

import { takeFirst } from "@ctrlplane/db";
import * as schema from "@ctrlplane/db/schema";
import { Permission } from "@ctrlplane/validators/auth";

import { authn, authz } from "../auth";
import { parseBody } from "../body-parser";
import { request } from "../middleware";

const body = schema.createEnvironment;
const body = schema.createEnvironment.extend({
expiresAt: z.coerce
.date()
.min(new Date(), "Expires at must be in the future")
.optional(),
});

export const POST = request()
.use(authn)
Expand All @@ -23,12 +30,22 @@ export const POST = request()
),
)
.handle<{ user: User; can: PermissionChecker; body: z.infer<typeof body> }>(
async (ctx) => {
const environment = await ctx.db
async (ctx) =>
ctx.db
.insert(schema.environment)
.values(ctx.body)
.returning();

return NextResponse.json({ environment });
},
.values({
...ctx.body,
expiresAt: isPresent(ctx.body.expiresAt)
? new Date(ctx.body.expiresAt)
: undefined,
})
.returning()
.then(takeFirst)
.then((environment) => NextResponse.json({ environment }))
.catch(() =>
NextResponse.json(
{ error: "Failed to create environment" },
{ status: 500 },
),
),
);
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";

import { and, eq, isNull, notInArray } from "@ctrlplane/db";
import { and, eq, notInArray } from "@ctrlplane/db";
import { db } from "@ctrlplane/db/client";
import {
deployment,
Expand Down Expand Up @@ -40,7 +40,6 @@ export const GET = async (
"completed",
"invalid_job_agent",
]),
isNull(environment.deletedAt),
),
)
.then((rows) =>
Expand Down
4 changes: 2 additions & 2 deletions apps/webservice/src/app/api/v1/jobs/[jobId]/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";

import { and, eq, isNull, takeFirst, takeFirstOrNull } from "@ctrlplane/db";
import { and, eq, takeFirst, takeFirstOrNull } from "@ctrlplane/db";
import { db } from "@ctrlplane/db/client";
import {
deployment,
Expand Down Expand Up @@ -76,7 +76,7 @@ export const GET = async (
.leftJoin(target, eq(target.id, releaseJobTrigger.targetId))
.leftJoin(release, eq(release.id, releaseJobTrigger.releaseId))
.leftJoin(deployment, eq(deployment.id, release.deploymentId))
.where(and(eq(job.id, params.jobId), isNull(environment.deletedAt)))
.where(eq(job.id, params.jobId))
.then(takeFirst)
.then((row) => ({
job: row.job,
Expand Down
2 changes: 0 additions & 2 deletions packages/api/src/router/environment-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
buildConflictUpdateColumns,
eq,
inArray,
isNull,
sql,
takeFirst,
takeFirstOrNull,
Expand Down Expand Up @@ -192,7 +191,6 @@ export const policyRouter = createTRPCRouter({
.where(
and(
eq(environmentPolicyApproval.id, envApproval.id),
isNull(environment.deletedAt),
eq(release.id, input.releaseId),
eq(job.status, JobStatus.Pending),
),
Expand Down
9 changes: 2 additions & 7 deletions packages/api/src/router/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
buildConflictUpdateColumns,
eq,
inArray,
isNull,
not,
takeFirst,
} from "@ctrlplane/db";
Expand All @@ -33,9 +32,7 @@ import { policyRouter } from "./environment-policy";
export const createEnv = async (
db: Tx,
input: z.infer<typeof createEnvironment>,
) => {
return db.insert(environment).values(input).returning().then(takeFirst);
};
) => db.insert(environment).values(input).returning().then(takeFirst);

export const environmentRouter = createTRPCRouter({
policy: policyRouter,
Expand Down Expand Up @@ -150,9 +147,7 @@ export const environmentRouter = createTRPCRouter({
.from(environment)
.innerJoin(system, eq(system.id, environment.systemId))
.orderBy(environment.name)
.where(
and(eq(environment.systemId, input), isNull(environment.deletedAt)),
);
.where(eq(environment.systemId, input));

return await Promise.all(
envs.map(async (e) => ({
Expand Down
Loading

0 comments on commit 4a0a13e

Please sign in to comment.