Skip to content

Commit

Permalink
fix: Hooks reference resource + trigger hooks on filter change for env (
Browse files Browse the repository at this point in the history
  • Loading branch information
adityachoudhari26 authored Nov 18, 2024
1 parent 1dd153f commit 6a8b95d
Show file tree
Hide file tree
Showing 9 changed files with 110 additions and 60 deletions.
62 changes: 61 additions & 1 deletion packages/api/src/router/environment.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Tx } from "@ctrlplane/db";
import type { ResourceCondition } from "@ctrlplane/validators/resources";
import _ from "lodash";
import { isPresent } from "ts-is-present";
import { z } from "zod";
Expand All @@ -8,6 +9,8 @@ import {
buildConflictUpdateColumns,
eq,
inArray,
isNotNull,
ne,
not,
takeFirst,
} from "@ctrlplane/db";
Expand All @@ -26,6 +29,10 @@ import {
import { getEventsForEnvironmentDeleted, handleEvent } from "@ctrlplane/events";
import { dispatchJobsForNewResources } from "@ctrlplane/job-dispatch";
import { Permission } from "@ctrlplane/validators/auth";
import {
ComparisonOperator,
FilterType,
} from "@ctrlplane/validators/conditions";

import { createTRPCRouter, protectedProcedure } from "../trpc";
import { policyRouter } from "./environment-policy";
Expand Down Expand Up @@ -234,21 +241,74 @@ export const environmentRouter = createTRPCRouter({
);

if (hasResourceFiltersChanged) {
const isOtherEnv = and(
isNotNull(environment.resourceFilter),
ne(environment.id, input.id),
);
const sys = await ctx.db.query.system.findFirst({
where: eq(system.id, oldEnv.system.id),
with: { environments: { where: isOtherEnv }, deployments: true },
});

const otherEnvFilters =
sys?.environments.map((e) => e.resourceFilter).filter(isPresent) ??
[];

const oldQuery = resourceMatchesMetadata(
ctx.db,
oldEnv.environment.resourceFilter,
);
const newQuery = resourceMatchesMetadata(ctx.db, resourceFilter);

const newResources = await ctx.db
.select({ id: resource.id })
.from(resource)
.where(
and(
eq(resource.workspaceId, oldEnv.system.workspaceId),
resourceMatchesMetadata(ctx.db, resourceFilter),
newQuery,
oldQuery && not(oldQuery),
),
);

const removedResources = await ctx.db.query.resource.findMany({
where: and(
eq(resource.workspaceId, oldEnv.system.workspaceId),
oldQuery,
newQuery && not(newQuery),
),
});

if (removedResources.length > 0) {
const sysFilter: ResourceCondition = {
type: FilterType.Comparison,
operator: ComparisonOperator.Or,
not: true,
conditions: otherEnvFilters,
};

const removedFromSystemResources =
await ctx.db.query.resource.findMany({
where: and(
inArray(
resource.id,
removedResources.map((r) => r.id),
),
resourceMatchesMetadata(ctx.db, sysFilter),
),
});

const events = removedFromSystemResources.flatMap((resource) =>
(sys?.deployments ?? []).map((deployment) => ({
action: "deployment.resource.removed" as const,
payload: { deployment, resource },
})),
);

const handleEventPromises = events.map(handleEvent);
await Promise.allSettled(handleEventPromises);
}

if (newResources.length > 0) {
await dispatchJobsForNewResources(
ctx.db,
Expand Down
4 changes: 2 additions & 2 deletions packages/api/src/router/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
takeFirstOrNull,
} from "@ctrlplane/db";
import * as schema from "@ctrlplane/db/schema";
import { getEventsForTargetDeleted, handleEvent } from "@ctrlplane/events";
import { getEventsForResourceDeleted, handleEvent } from "@ctrlplane/events";
import {
cancelOldReleaseJobTriggersOnJobDispatch,
createJobApprovals,
Expand Down Expand Up @@ -582,7 +582,7 @@ export const resourceRouter = createTRPCRouter({
where: inArray(schema.resource.id, input),
});
const events = (
await Promise.allSettled(resources.map(getEventsForTargetDeleted))
await Promise.allSettled(resources.map(getEventsForResourceDeleted))
).flatMap((r) => (r.status === "fulfilled" ? r.value : []));
await Promise.allSettled(events.map(handleEvent));

Expand Down
16 changes: 8 additions & 8 deletions packages/events/src/handlers/target-removed.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import type { TargetRemoved } from "@ctrlplane/validators/events";
import type { ResourceRemoved } from "@ctrlplane/validators/events";

import { and, eq } from "@ctrlplane/db";
import { db } from "@ctrlplane/db/client";
import * as SCHEMA from "@ctrlplane/db/schema";
import { dispatchRunbook } from "@ctrlplane/job-dispatch";

export const handleTargetRemoved = async (event: TargetRemoved) => {
const { target, deployment } = event.payload;
export const handleResourceRemoved = async (event: ResourceRemoved) => {
const { resource, deployment } = event.payload;

const isSubscribedToTargetRemoved = and(
const isSubscribedToResourceRemoved = and(
eq(SCHEMA.hook.scopeId, deployment.id),
eq(SCHEMA.hook.scopeType, "deployment"),
eq(SCHEMA.hook.action, "deployment.target.removed"),
eq(SCHEMA.hook.action, "deployment.resource.removed"),
);
const runhooks = await db
.select()
.from(SCHEMA.runhook)
.innerJoin(SCHEMA.hook, eq(SCHEMA.runhook.hookId, SCHEMA.hook.id))
.where(isSubscribedToTargetRemoved);
.where(isSubscribedToResourceRemoved);

const targetId = target.id;
const resourceId = resource.id;
const deploymentId = deployment.id;
const handleRunhooksPromises = runhooks.map(({ runhook }) =>
dispatchRunbook(db, runhook.runbookId, { targetId, deploymentId }),
dispatchRunbook(db, runhook.runbookId, { resourceId, deploymentId }),
);

await Promise.all(handleRunhooksPromises);
Expand Down
8 changes: 4 additions & 4 deletions packages/events/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import type { HookEvent } from "@ctrlplane/validators/events";

import { db } from "@ctrlplane/db/client";
import * as SCHEMA from "@ctrlplane/db/schema";
import { isTargetRemoved } from "@ctrlplane/validators/events";
import { isResourceRemoved } from "@ctrlplane/validators/events";

import { handleTargetRemoved } from "./handlers/target-removed.js";
import { handleResourceRemoved } from "./handlers/index.js";

export * from "./triggers/index.js";
export * from "./handlers/index.js";

export const handleEvent = async (event: HookEvent) => {
await db.insert(SCHEMA.event).values(event);
if (isTargetRemoved(event)) return handleTargetRemoved(event);
throw new Error(`Unhandled event: ${event.action}`);
if (isResourceRemoved(event)) return handleResourceRemoved(event);
throw new Error(`Unhandled event`);
};
8 changes: 4 additions & 4 deletions packages/events/src/triggers/deployment-deleted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ export const getEventsForDeploymentDeleted = async (
conditions: envFilters,
};

const targets = await db.query.resource.findMany({
const resources = await db.query.resource.findMany({
where: SCHEMA.resourceMatchesMetadata(db, systemFilter),
});

return targets.map((target) => ({
action: "deployment.target.removed",
payload: { deployment, target },
return resources.map((resource) => ({
action: "deployment.resource.removed",
payload: { deployment, resource },
}));
};
18 changes: 9 additions & 9 deletions packages/events/src/triggers/environment-deleted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ export const getEventsForEnvironmentDeleted = async (
environment: SCHEMA.Environment,
): Promise<HookEvent[]> => {
if (environment.resourceFilter == null) return [];
const targets = await db
const resources = await db
.select()
.from(SCHEMA.resource)
.where(SCHEMA.resourceMatchesMetadata(db, environment.resourceFilter));
if (targets.length === 0) return [];
if (resources.length === 0) return [];

const checks = and(
isNotNull(SCHEMA.environment.resourceFilter),
Expand All @@ -39,7 +39,7 @@ export const getEventsForEnvironmentDeleted = async (
conditions: envFilters,
};

const removedFromSystemTargets =
const removedFromSystemResources =
envFilters.length > 0
? await db
.select()
Expand All @@ -49,17 +49,17 @@ export const getEventsForEnvironmentDeleted = async (
SCHEMA.resourceMatchesMetadata(db, removedFromSystemFilter),
inArray(
SCHEMA.resource.id,
targets.map((t) => t.id),
resources.map((r) => r.id),
),
),
)
: targets;
if (removedFromSystemTargets.length === 0) return [];
: resources;
if (removedFromSystemResources.length === 0) return [];

return system.deployments.flatMap((deployment) =>
removedFromSystemTargets.map((target) => ({
action: "deployment.target.removed",
payload: { deployment, target },
removedFromSystemResources.map((resource) => ({
action: "deployment.resource.removed",
payload: { deployment, resource },
})),
);
};
22 changes: 11 additions & 11 deletions packages/events/src/triggers/target-deleted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ import { ComparisonOperator } from "@ctrlplane/validators/conditions";
import { ResourceFilterType } from "@ctrlplane/validators/resources";

/**
* Get events for a target that has been deleted.
* NOTE: Because we may need to do inner joins on target metadata for the filter,
* this actually needs to be called before the target is actually deleted.
* @param target
* Get events for a resource that has been deleted.
* NOTE: Because we may need to do inner joins on resource metadata for the filter,
* this actually needs to be called before the resource is actually deleted.
* @param resource
*/
export const getEventsForTargetDeleted = async (
target: SCHEMA.Resource,
export const getEventsForResourceDeleted = async (
resource: SCHEMA.Resource,
): Promise<HookEvent[]> => {
const systems = await db.query.system.findMany({
where: eq(SCHEMA.system.workspaceId, target.workspaceId),
where: eq(SCHEMA.system.workspaceId, resource.workspaceId),
with: {
environments: { where: isNotNull(SCHEMA.environment.resourceFilter) },
deployments: true,
Expand All @@ -36,17 +36,17 @@ export const getEventsForTargetDeleted = async (
conditions: filters,
};

const matchedTarget = await db.query.resource.findFirst({
const matchedResource = await db.query.resource.findFirst({
where: SCHEMA.resourceMatchesMetadata(db, systemFilter),
});
if (matchedTarget == null) return [];
if (matchedResource == null) return [];

return s.deployments;
});
const deployments = (await Promise.all(deploymentPromises)).flat();

return deployments.map((deployment) => ({
action: "deployment.target.removed",
payload: { target, deployment },
action: "deployment.resource.removed",
payload: { resource, deployment },
}));
};
16 changes: 6 additions & 10 deletions packages/validators/src/events/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
import { z } from "zod";

import type { TargetDeleted, TargetRemoved } from "./target.js";
import { targetDeleted, targetRemoved } from "./target.js";
import type { ResourceRemoved } from "./target.js";
import { resourceRemoved } from "./target.js";

export * from "./target.js";

export const hookEvent = z.union([targetRemoved, targetDeleted]);
export const hookEvent = resourceRemoved;
export type HookEvent = z.infer<typeof hookEvent>;

// typeguards
export const isTargetRemoved = (event: HookEvent): event is TargetRemoved =>
event.action === "deployment.target.removed";
export const isTargetDeleted = (event: HookEvent): event is TargetDeleted =>
event.action === "deployment.target.deleted";
export const isResourceRemoved = (event: HookEvent): event is ResourceRemoved =>
true;

// action
export const hookActionsList = hookEvent.options.map(
(schema) => schema.shape.action.value,
);
export const hookActionsList = ["deployment.resource.removed"];
export const hookActions = z.enum(hookActionsList as [string, ...string[]]);
16 changes: 5 additions & 11 deletions packages/validators/src/events/hooks/target.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
import { z } from "zod";

const deployment = z.object({ id: z.string().uuid(), name: z.string() });
const target = z.object({
const resource = z.object({
id: z.string().uuid(),
name: z.string(),
config: z.record(z.any()),
});

export const targetRemoved = z.object({
action: z.literal("deployment.target.removed"),
payload: z.object({ deployment, target }),
export const resourceRemoved = z.object({
action: z.literal("deployment.resource.removed"),
payload: z.object({ deployment, resource }),
});
export type TargetRemoved = z.infer<typeof targetRemoved>;

export const targetDeleted = z.object({
action: z.literal("deployment.target.deleted"),
payload: z.object({ target }),
});
export type TargetDeleted = z.infer<typeof targetDeleted>;
export type ResourceRemoved = z.infer<typeof resourceRemoved>;

0 comments on commit 6a8b95d

Please sign in to comment.