- {deployments.data?.map((deployment) => {
- return (
-
-
{deployment.name}
-
-
-
-
- Keys
- Value
-
-
-
- {variables.data
- ?.filter((v) => v.deploymentId === deployment.id)
- .map((v) => (
-
- ))}
-
-
-
- );
- })}
+
+
+
Deployment Variables
+ {deployments.data
+ ?.filter((d) => {
+ const vars = variables.data?.filter((v) => v.deploymentId === d.id);
+ return vars != null && vars.length > 0;
+ })
+ .map((deployment) => {
+ return (
+
+
{deployment.name}
+
+
+
+ Keys
+ Value
+
+
+
+ {variables.data
+ ?.filter((v) => v.deploymentId === deployment.id)
+ .map((v) => {
+ const targetVar = targetVariables.find(
+ (tv) => tv.key === v.key,
+ );
+ return (
+
+ );
+ })}
+
+
+
+ );
+ })}
);
};
diff --git a/packages/api/package.json b/packages/api/package.json
index c0dd467de..691eef381 100644
--- a/packages/api/package.json
+++ b/packages/api/package.json
@@ -23,6 +23,7 @@
"@ctrlplane/db": "workspace:*",
"@ctrlplane/job-dispatch": "workspace:*",
"@ctrlplane/logger": "workspace:*",
+ "@ctrlplane/secrets": "workspace:*",
"@ctrlplane/validators": "workspace:*",
"@octokit/auth-app": "^7.1.0",
"@octokit/rest": "catalog:",
diff --git a/packages/api/src/router/target.ts b/packages/api/src/router/target.ts
index 3911c1adf..d670a487c 100644
--- a/packages/api/src/router/target.ts
+++ b/packages/api/src/router/target.ts
@@ -15,6 +15,7 @@ import {
takeFirstOrNull,
} from "@ctrlplane/db";
import * as schema from "@ctrlplane/db/schema";
+import { variablesAES256 } from "@ctrlplane/secrets";
import { Permission } from "@ctrlplane/validators/auth";
import { targetCondition } from "@ctrlplane/validators/targets";
@@ -184,6 +185,79 @@ const targetViews = createTRPCRouter({
}),
});
+const targetVariables = createTRPCRouter({
+ create: protectedProcedure
+ .input(schema.createTargetVariable)
+ .meta({
+ authorizationCheck: ({ canUser, input }) =>
+ canUser
+ .perform(Permission.TargetUpdate)
+ .on({ type: "target", id: input.targetId }),
+ })
+ .mutation(async ({ ctx, input }) => {
+ const { sensitive } = input;
+ const value = sensitive
+ ? variablesAES256().encrypt(String(input.value))
+ : input.value;
+ const data = { ...input, value };
+ return ctx.db.insert(schema.targetVariable).values(data).returning();
+ }),
+
+ update: protectedProcedure
+ .input(
+ z.object({ id: z.string().uuid(), data: schema.updateTargetVariable }),
+ )
+ .meta({
+ authorizationCheck: async ({ ctx, canUser, input }) => {
+ const variable = await ctx.db
+ .select()
+ .from(schema.targetVariable)
+ .where(eq(schema.targetVariable.id, input.id))
+ .then(takeFirstOrNull);
+ if (!variable) return false;
+
+ return canUser
+ .perform(Permission.TargetUpdate)
+ .on({ type: "target", id: variable.targetId });
+ },
+ })
+ .mutation(async ({ ctx, input }) => {
+ const { sensitive } = input.data;
+ const value = sensitive
+ ? variablesAES256().encrypt(String(input.data.value))
+ : input.data.value;
+ const data = { ...input.data, value };
+ return ctx.db
+ .update(schema.targetVariable)
+ .set(data)
+ .where(eq(schema.targetVariable.id, input.id))
+ .returning()
+ .then(takeFirst);
+ }),
+
+ delete: protectedProcedure
+ .input(z.string().uuid())
+ .meta({
+ authorizationCheck: async ({ ctx, canUser, input }) => {
+ const variable = await ctx.db
+ .select()
+ .from(schema.targetVariable)
+ .where(eq(schema.targetVariable.id, input))
+ .then(takeFirstOrNull);
+ if (!variable) return false;
+
+ return canUser
+ .perform(Permission.TargetUpdate)
+ .on({ type: "target", id: variable.targetId });
+ },
+ })
+ .mutation(async ({ ctx, input }) =>
+ ctx.db
+ .delete(schema.targetVariable)
+ .where(eq(schema.targetVariable.id, input)),
+ ),
+});
+
type _StringStringRecord = Record
;
const targetQuery = (db: Tx, checks: Array>) =>
db
@@ -218,6 +292,7 @@ export const targetRouter = createTRPCRouter({
provider: targetProviderRouter,
relations: targetRelations,
view: targetViews,
+ variable: targetVariables,
byId: protectedProcedure
.meta({
@@ -225,27 +300,19 @@ export const targetRouter = createTRPCRouter({
canUser.perform(Permission.TargetGet).on({ type: "target", id: input }),
})
.input(z.string().uuid())
- .query(async ({ ctx, input }) => {
- const metadata = await ctx.db
- .select()
- .from(schema.targetMetadata)
- .where(eq(schema.targetMetadata.targetId, input))
- .then((lbs) => Object.fromEntries(lbs.map((lb) => [lb.key, lb.value])));
- return ctx.db
- .select()
- .from(schema.target)
- .leftJoin(
- schema.targetProvider,
- eq(schema.target.providerId, schema.targetProvider.id),
- )
- .where(eq(schema.target.id, input))
- .then(takeFirstOrNull)
- .then((a) =>
- a == null
- ? null
- : { ...a.target, metadata, provider: a.target_provider },
- );
- }),
+ .query(({ ctx, input }) =>
+ ctx.db.query.target
+ .findFirst({
+ where: eq(schema.target.id, input),
+ with: { metadata: true, variables: true, provider: true },
+ })
+ .then((t) => {
+ if (t == null) return null;
+ const pairs = t.metadata.map((m) => [m.key, m.value]);
+ const metadata = Object.fromEntries(pairs);
+ return { ...t, metadata };
+ }),
+ ),
byWorkspaceId: createTRPCRouter({
list: protectedProcedure
diff --git a/packages/db/src/schema/target-provider.ts b/packages/db/src/schema/target-provider.ts
index 01fd8c5e9..cc2279aa0 100644
--- a/packages/db/src/schema/target-provider.ts
+++ b/packages/db/src/schema/target-provider.ts
@@ -1,4 +1,5 @@
import type { InferSelectModel } from "drizzle-orm";
+import { relations } from "drizzle-orm";
import {
pgTable,
text,
@@ -9,6 +10,7 @@ import {
import { createInsertSchema } from "drizzle-zod";
import { z } from "zod";
+import { target } from "./target.js";
import { workspace } from "./workspace.js";
export const targetProvider = pgTable(
@@ -24,6 +26,11 @@ export const targetProvider = pgTable(
(t) => ({ uniq: uniqueIndex().on(t.workspaceId, t.name) }),
);
+export const targetProviderRelations = relations(
+ targetProvider,
+ ({ many }) => ({ targets: many(target) }),
+);
+
export const createTargetProvider = createInsertSchema(targetProvider).omit({
id: true,
});
diff --git a/packages/db/src/schema/target.ts b/packages/db/src/schema/target.ts
index 1f029fa29..2d0b18a04 100644
--- a/packages/db/src/schema/target.ts
+++ b/packages/db/src/schema/target.ts
@@ -3,7 +3,7 @@ import type {
TargetCondition,
} from "@ctrlplane/validators/targets";
import type { InferInsertModel, InferSelectModel, SQL } from "drizzle-orm";
-import { exists, like, not, notExists, or, sql } from "drizzle-orm";
+import { exists, like, not, notExists, or, relations, sql } from "drizzle-orm";
import {
boolean,
json,
@@ -51,6 +51,15 @@ export const target = pgTable(
(t) => ({ uniq: uniqueIndex().on(t.identifier, t.workspaceId) }),
);
+export const targetRelations = relations(target, ({ one, many }) => ({
+ metadata: many(targetMetadata),
+ variables: many(targetVariable),
+ provider: one(targetProvider, {
+ fields: [target.providerId],
+ references: [targetProvider.id],
+ }),
+}));
+
export type Target = InferSelectModel;
export const createTarget = createInsertSchema(target, {
@@ -110,6 +119,13 @@ export const targetMetadata = pgTable(
(t) => ({ uniq: uniqueIndex().on(t.key, t.targetId) }),
);
+export const targetMetadataRelations = relations(targetMetadata, ({ one }) => ({
+ target: one(target, {
+ fields: [targetMetadata.targetId],
+ references: [target.id],
+ }),
+}));
+
const buildMetadataCondition = (tx: Tx, cond: MetadataCondition): SQL => {
if (cond.operator === "null")
return notExists(
@@ -227,15 +243,22 @@ export const targetVariable = pgTable(
.notNull(),
key: text("key").notNull(),
- value: jsonb("value").notNull(),
+ value: jsonb("value").$type().notNull(),
sensitive: boolean("sensitive").notNull().default(false),
},
(t) => ({ uniq: uniqueIndex().on(t.targetId, t.key) }),
);
-export const createTargetVariable = createInsertSchema(targetVariable).omit({
- id: true,
-});
+export const targetVariableRelations = relations(targetVariable, ({ one }) => ({
+ target: one(target, {
+ fields: [targetVariable.targetId],
+ references: [target.id],
+ }),
+}));
+
+export const createTargetVariable = createInsertSchema(targetVariable, {
+ value: z.union([z.string(), z.number(), z.boolean()]),
+}).omit({ id: true });
export const updateTargetVariable = createTargetVariable.partial();
export type TargetVariable = InferSelectModel;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6fa80037d..d5f564204 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -890,6 +890,9 @@ importers:
'@ctrlplane/logger':
specifier: workspace:*
version: link:../logger
+ '@ctrlplane/secrets':
+ specifier: workspace:*
+ version: link:../secrets
'@ctrlplane/validators':
specifier: workspace:*
version: link:../validators