diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/policies/[policyId]/edit/_components/PolicyFormContext.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/[policyId]/edit/_components/PolicyFormContext.tsx index 7ce7ad8b9..645e5b700 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/policies/[policyId]/edit/_components/PolicyFormContext.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/[policyId]/edit/_components/PolicyFormContext.tsx @@ -24,6 +24,7 @@ type Policy = SCHEMA.Policy & { versionUserApprovals: SCHEMA.PolicyRuleUserApproval[]; versionRoleApprovals: SCHEMA.PolicyRuleRoleApproval[]; concurrency: SCHEMA.PolicyRuleConcurrency | null; + environmentVersionRollout: SCHEMA.PolicyRuleEnvironmentVersionRollout | null; }; type PolicyFormContextType = { @@ -47,14 +48,22 @@ export const PolicyFormContextProvider: React.FC<{ policy: Policy; }> = ({ children, policy }) => { const concurrency = policy.concurrency?.concurrency ?? null; - const defaultValues = { ...policy, concurrency }; + const environmentVersionRollout = + policy.environmentVersionRollout != null + ? { + ...policy.environmentVersionRollout, + rolloutType: + SCHEMA.dbRolloutTypeToAPIRolloutType[ + policy.environmentVersionRollout.rolloutType + ], + } + : null; + const defaultValues = { ...policy, concurrency, environmentVersionRollout }; const form = useForm({ schema: SCHEMA.updatePolicy, defaultValues, }); - console.log(form.getValues()); - const router = useRouter(); const utils = api.useUtils(); diff --git a/apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/approve/openapi.ts b/apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/approve/openapi.ts index 9019e53b3..e6ea87e41 100644 --- a/apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/approve/openapi.ts +++ b/apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/approve/openapi.ts @@ -51,6 +51,11 @@ export const openapi: Swagger.SwaggerV3 = { type: "object", properties: { reason: { type: "string" }, + approvedAt: { + type: "string", + format: "date-time", + nullable: true, + }, }, }, }, diff --git a/apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/approve/route.ts b/apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/approve/route.ts index a613b350f..57b24a028 100644 --- a/apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/approve/route.ts +++ b/apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/approve/route.ts @@ -13,6 +13,7 @@ import { request } from "~/app/api/v1/middleware"; const bodySchema = z.object({ reason: z.string().optional(), + approvedAt: z.string().datetime().optional(), }); export const POST = request() @@ -42,6 +43,8 @@ export const POST = request() { status: NOT_FOUND }, ); + const approvedAt = + ctx.body.approvedAt != null ? new Date(ctx.body.approvedAt) : new Date(); const record = await ctx.db .insert(schema.policyRuleAnyApprovalRecord) .values({ @@ -49,7 +52,7 @@ export const POST = request() userId: ctx.user.id, status: schema.ApprovalStatus.Approved, reason: ctx.body.reason, - approvedAt: new Date(), + approvedAt, }) .onConflictDoNothing() .returning(); diff --git a/apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/environments/[environmentId]/rollout/openapi.ts b/apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/environments/[environmentId]/rollout/openapi.ts new file mode 100644 index 000000000..690a995e9 --- /dev/null +++ b/apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/environments/[environmentId]/rollout/openapi.ts @@ -0,0 +1,84 @@ +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/deployment-versions/{deploymentVersionId}/environments/{environmentId}/rollout": + { + get: { + summary: + "Get the rollout information across all release targets for a given deployment version and environment", + operationId: "getRolloutInfo", + parameters: [ + { + name: "deploymentVersionId", + in: "path", + required: true, + schema: { type: "string", format: "uuid" }, + description: "The deployment version ID", + }, + { + name: "environmentId", + in: "path", + required: true, + schema: { type: "string", format: "uuid" }, + description: "The environment ID", + }, + ], + responses: { + 200: { + description: "The rollout information", + content: { + "application/json": { + schema: { + type: "array", + items: { + allOf: [ + { $ref: "#/components/schemas/ReleaseTarget" }, + { + type: "object", + properties: { + rolloutTime: { + type: "string", + format: "date-time", + nullable: true, + }, + rolloutPosition: { type: "number" }, + }, + required: ["rolloutTime", "rolloutPosition"], + }, + ], + }, + }, + }, + }, + }, + 404: { + description: + "The deployment version or environment was 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" } }, + }, + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/environments/[environmentId]/rollout/route.ts b/apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/environments/[environmentId]/rollout/route.ts new file mode 100644 index 000000000..4f3e4922e --- /dev/null +++ b/apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/environments/[environmentId]/rollout/route.ts @@ -0,0 +1,143 @@ +import type { Tx } from "@ctrlplane/db"; +import { NextResponse } from "next/server"; +import { INTERNAL_SERVER_ERROR, NOT_FOUND } from "http-status"; + +import { and, eq, takeFirstOrNull } from "@ctrlplane/db"; +import * as schema from "@ctrlplane/db/schema"; +import { logger } from "@ctrlplane/logger"; +import { + getRolloutInfoForReleaseTarget, + mergePolicies, +} from "@ctrlplane/rule-engine"; +import { getApplicablePoliciesWithoutResourceScope } from "@ctrlplane/rule-engine/db"; +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({ + route: + "/v1/deployment-versions/{deploymentVersionId}/environments/{environmentId}/rollout", +}); + +const getDeploymentVersion = async (db: Tx, deploymentVersionId: string) => + db + .select() + .from(schema.deploymentVersion) + .where(eq(schema.deploymentVersion.id, deploymentVersionId)) + .then(takeFirstOrNull); + +const getEnvironment = async (db: Tx, environmentId: string) => + db + .select() + .from(schema.environment) + .where(eq(schema.environment.id, environmentId)) + .then(takeFirstOrNull); + +const getReleaseTargets = async ( + db: Tx, + deploymentId: string, + environmentId: string, +) => + db + .select() + .from(schema.releaseTarget) + .innerJoin( + schema.deployment, + eq(schema.releaseTarget.deploymentId, schema.deployment.id), + ) + .innerJoin( + schema.environment, + eq(schema.releaseTarget.environmentId, schema.environment.id), + ) + .innerJoin( + schema.resource, + eq(schema.releaseTarget.resourceId, schema.resource.id), + ) + .where( + and( + eq(schema.releaseTarget.deploymentId, deploymentId), + eq(schema.releaseTarget.environmentId, environmentId), + ), + ) + .then((rows) => + rows.map((row) => ({ + ...row.release_target, + deployment: row.deployment, + environment: row.environment, + resource: row.resource, + })), + ); + +export const GET = request() + .use(authn) + .use( + authz(({ can, params }) => + can.perform(Permission.DeploymentVersionGet).on({ + type: "deploymentVersion", + id: params.deploymentVersionId ?? "", + }), + ), + ) + .handle< + { db: Tx }, + { params: Promise<{ deploymentVersionId: string; environmentId: string }> } + >(async ({ db }, { params }) => { + try { + const { deploymentVersionId, environmentId } = await params; + + const deploymentVersion = await getDeploymentVersion( + db, + deploymentVersionId, + ); + if (deploymentVersion == null) + return NextResponse.json( + { error: "Deployment version not found" }, + { status: NOT_FOUND }, + ); + + const environment = await getEnvironment(db, environmentId); + if (environment == null) + return NextResponse.json( + { error: "Environment not found" }, + { status: NOT_FOUND }, + ); + + const releaseTargets = await getReleaseTargets( + db, + deploymentVersion.deploymentId, + environmentId, + ); + + const policies = await getApplicablePoliciesWithoutResourceScope( + db, + environmentId, + deploymentVersion.deploymentId, + ); + const policy = mergePolicies(policies); + + const releaseTargetsWithRolloutInfo = await Promise.all( + releaseTargets.map((releaseTarget) => + getRolloutInfoForReleaseTarget( + db, + releaseTarget, + policy, + deploymentVersion, + ), + ), + ); + + const releaseTargetsSortedByRolloutPosition = + releaseTargetsWithRolloutInfo.sort( + (a, b) => a.rolloutPosition - b.rolloutPosition, + ); + + return NextResponse.json(releaseTargetsSortedByRolloutPosition); + } catch (error) { + log.error("Error getting rollout info", { error }); + return NextResponse.json( + { error: "Internal server error" }, + { status: INTERNAL_SERVER_ERROR }, + ); + } + }); diff --git a/apps/webservice/src/app/api/v1/openapi.ts b/apps/webservice/src/app/api/v1/openapi.ts index 7f1dbd381..c3625cdfd 100644 --- a/apps/webservice/src/app/api/v1/openapi.ts +++ b/apps/webservice/src/app/api/v1/openapi.ts @@ -145,71 +145,6 @@ export const openapi: Swagger.SwaggerV3 = { "jobAgentConfig", ], }, - Policy: { - type: "object", - properties: { - id: { type: "string", format: "uuid", description: "The policy ID" }, - systemId: { - type: "string", - format: "uuid", - description: "The system ID", - }, - name: { type: "string", description: "The name of the policy" }, - description: { - type: "string", - nullable: true, - description: "The description of the policy", - }, - approvalRequirement: { - type: "string", - enum: ["manual", "automatic"], - description: "The approval requirement of the policy", - }, - successType: { - type: "string", - enum: ["some", "all", "optional"], - description: - "If a policy depends on an environment, whether or not the policy requires all, some, or optional successful releases in the environment", - }, - successMinimum: { - type: "number", - description: - "If a policy depends on an environment, the minimum number of successful releases in the environment", - }, - concurrencyLimit: { - type: "number", - nullable: true, - description: - "The maximum number of concurrent releases in the environment", - }, - rolloutDuration: { - type: "number", - description: "The duration of the rollout in milliseconds", - }, - minimumReleaseInterval: { - type: "number", - description: - "The minimum interval between releases in milliseconds", - }, - releaseSequencing: { - type: "string", - enum: ["wait", "cancel"], - description: - "If a new release is created, whether it will wait for the current release to finish before starting, or cancel the current release", - }, - }, - required: [ - "id", - "systemId", - "name", - "approvalRequirement", - "successType", - "successMinimum", - "rolloutDuration", - "minimumReleaseInterval", - "releaseSequencing", - ], - }, Environment: { type: "object", properties: { diff --git a/apps/webservice/src/app/api/v1/policies/[policyId]/openapi.ts b/apps/webservice/src/app/api/v1/policies/[policyId]/openapi.ts index 77375437a..6bb5caf45 100644 --- a/apps/webservice/src/app/api/v1/policies/[policyId]/openapi.ts +++ b/apps/webservice/src/app/api/v1/policies/[policyId]/openapi.ts @@ -9,6 +9,55 @@ export const openapi: Swagger.SwaggerV3 = { paths: { "/v1/policies/{policyId}": { + get: { + summary: "Get a policy", + operationId: "getPolicy", + parameters: [ + { + name: "policyId", + in: "path", + required: true, + schema: { type: "string", format: "uuid" }, + }, + ], + responses: { + 200: { + description: "OK", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Policy" }, + }, + }, + }, + 404: { + description: "Policy 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" }, + }, + }, + }, + }, + }, + }, + }, + patch: { summary: "Update a policy", operationId: "updatePolicy", @@ -80,6 +129,9 @@ export const openapi: Swagger.SwaggerV3 = { concurrency: { $ref: "#/components/schemas/PolicyConcurrency", }, + environmentVersionRollout: { + $ref: "#/components/schemas/EnvironmentVersionRollout", + }, }, }, }, diff --git a/apps/webservice/src/app/api/v1/policies/[policyId]/route.ts b/apps/webservice/src/app/api/v1/policies/[policyId]/route.ts index 1d6d1ad51..f5c1a7e6c 100644 --- a/apps/webservice/src/app/api/v1/policies/[policyId]/route.ts +++ b/apps/webservice/src/app/api/v1/policies/[policyId]/route.ts @@ -16,6 +16,67 @@ import { request } from "../../middleware"; const log = logger.child({ route: "/api/v1/policies/[policyId]" }); +export const GET = request() + .use(authn) + .use( + authz(({ can, params }) => + can + .perform(Permission.PolicyGet) + .on({ type: "policy", id: params.policyId ?? "" }), + ), + ) + .handle }>( + async ({ db }, { params }) => { + try { + const { policyId } = await params; + + const policy = await db.query.policy.findFirst({ + where: eq(SCHEMA.policy.id, policyId), + with: { + targets: true, + denyWindows: true, + deploymentVersionSelector: true, + versionAnyApprovals: true, + versionUserApprovals: true, + versionRoleApprovals: true, + concurrency: true, + environmentVersionRollout: true, + }, + }); + + const concurrency = policy?.concurrency?.concurrency; + const environmentVersionRollout = + policy?.environmentVersionRollout != null + ? { + ...policy.environmentVersionRollout, + rolloutType: + SCHEMA.dbRolloutTypeToAPIRolloutType[ + policy.environmentVersionRollout.rolloutType + ], + } + : null; + + if (policy == null) + return NextResponse.json( + { error: "Policy not found" }, + { status: NOT_FOUND }, + ); + + return NextResponse.json({ + ...policy, + concurrency, + environmentVersionRollout, + }); + } catch (error) { + log.error("Failed to get policy", { error }); + return NextResponse.json( + { error: "Failed to get policy" }, + { status: INTERNAL_SERVER_ERROR }, + ); + } + }, + ); + export const PATCH = request() .use(authn) .use( diff --git a/apps/webservice/src/app/api/v1/policies/openapi.ts b/apps/webservice/src/app/api/v1/policies/openapi.ts index 88d928bb6..55d4b037a 100644 --- a/apps/webservice/src/app/api/v1/policies/openapi.ts +++ b/apps/webservice/src/app/api/v1/policies/openapi.ts @@ -1,5 +1,7 @@ import type { Swagger } from "atlassian-openapi"; +import * as schema from "@ctrlplane/db/schema"; + export const openapi: Swagger.SwaggerV3 = { openapi: "3.0.0", info: { @@ -73,6 +75,28 @@ export const openapi: Swagger.SwaggerV3 = { minimum: 1, format: "int32", }, + EnvironmentVersionRollout: { + type: "object", + properties: { + positionGrowthFactor: { + type: "number", + description: + "Controls how strongly queue position influences delay — higher values result in a smoother, slower rollout curve.", + }, + timeScaleInterval: { + type: "number", + description: + "Defines the base time interval that each unit of rollout progression is scaled by — larger values stretch the deployment timeline.", + }, + rolloutType: { + type: "string", + enum: Object.keys(schema.apiRolloutTypeToDBRolloutType), + description: + "Determines the shape of the rollout curve — linear, exponential, or normalized versions of each. A normalized rollout curve limits the maximum delay to the time scale interval, and scales the rollout progression to fit within that interval.", + }, + }, + required: ["positionGrowthFactor", "timeScaleInterval", "rolloutType"], + }, Policy: { type: "object", properties: { @@ -108,6 +132,9 @@ export const openapi: Swagger.SwaggerV3 = { concurrency: { $ref: "#/components/schemas/PolicyConcurrency", }, + environmentVersionRollout: { + $ref: "#/components/schemas/EnvironmentVersionRollout", + }, }, required: [ "id", diff --git a/e2e/api/entity-fixtures.ts b/e2e/api/entity-fixtures.ts index f9858eeb6..1ed68f58f 100644 --- a/e2e/api/entity-fixtures.ts +++ b/e2e/api/entity-fixtures.ts @@ -120,6 +120,18 @@ export const PolicyFixture = z.object({ }), ) .optional(), + environmentVersionRollout: z + .object({ + rolloutType: z.enum([ + "linear", + "linear-normalized", + "exponential", + "exponential-normalized", + ]), + positionGrowthFactor: z.number().default(1), + timeScaleInterval: z.number(), + }) + .optional(), }); export const AgentFixture = z.object({ diff --git a/e2e/api/schema.ts b/e2e/api/schema.ts index 6dab7aab1..ab2201f95 100644 --- a/e2e/api/schema.ts +++ b/e2e/api/schema.ts @@ -58,6 +58,23 @@ export interface paths { patch?: never; trace?: never; }; + "/v1/deployment-versions/{deploymentVersionId}/environments/{environmentId}/rollout": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get the rollout information across all release targets for a given deployment version and environment */ + get: operations["getRolloutInfo"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/v1/deployment-versions/{deploymentVersionId}": { parameters: { query?: never; @@ -379,7 +396,8 @@ export interface paths { path?: never; cookie?: never; }; - get?: never; + /** Get a policy */ + get: operations["getPolicy"]; put?: never; post?: never; /** Delete a policy */ @@ -1148,45 +1166,6 @@ export interface components { /** @enum {string} */ status?: "building" | "ready" | "failed"; }; - Policy: { - /** - * Format: uuid - * @description The policy ID - */ - id: string; - /** - * Format: uuid - * @description The system ID - */ - systemId: string; - /** @description The name of the policy */ - name: string; - /** @description The description of the policy */ - description?: string | null; - /** - * @description The approval requirement of the policy - * @enum {string} - */ - approvalRequirement: "manual" | "automatic"; - /** - * @description If a policy depends on an environment, whether or not the policy requires all, some, or optional successful releases in the environment - * @enum {string} - */ - successType: "some" | "all" | "optional"; - /** @description If a policy depends on an environment, the minimum number of successful releases in the environment */ - successMinimum: number; - /** @description The maximum number of concurrent releases in the environment */ - concurrencyLimit?: number | null; - /** @description The duration of the rollout in milliseconds */ - rolloutDuration: number; - /** @description The minimum interval between releases in milliseconds */ - minimumReleaseInterval: number; - /** - * @description If a new release is created, whether it will wait for the current release to finish before starting, or cancel the current release - * @enum {string} - */ - releaseSequencing: "wait" | "cancel"; - }; Environment: { /** Format: uuid */ id: string; @@ -1366,7 +1345,22 @@ export interface components { }; /** Format: int32 */ PolicyConcurrency: number | null; - Policy1: { + EnvironmentVersionRollout: { + /** @description Controls how strongly queue position influences delay — higher values result in a smoother, slower rollout curve. */ + positionGrowthFactor: number; + /** @description Defines the base time interval that each unit of rollout progression is scaled by — larger values stretch the deployment timeline. */ + timeScaleInterval: number; + /** + * @description Determines the shape of the rollout curve — linear, exponential, or normalized versions of each. A normalized rollout curve limits the maximum delay to the time scale interval, and scales the rollout progression to fit within that interval. + * @enum {string} + */ + rolloutType: + | "linear" + | "linear-normalized" + | "exponential" + | "exponential-normalized"; + }; + Policy: { /** Format: uuid */ id: string; name: string; @@ -1384,6 +1378,7 @@ export interface components { versionUserApprovals: components["schemas"]["VersionUserApproval"][]; versionRoleApprovals: components["schemas"]["VersionRoleApproval"][]; concurrency?: components["schemas"]["PolicyConcurrency"]; + environmentVersionRollout?: components["schemas"]["EnvironmentVersionRollout"]; }; UpdateResourceRelationshipRule: { name?: string; @@ -1609,6 +1604,8 @@ export interface operations { content: { "application/json": { reason?: string; + /** Format: date-time */ + approvedAt?: string | null; }; }; }; @@ -1657,6 +1654,57 @@ export interface operations { }; }; }; + getRolloutInfo: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The deployment version ID */ + deploymentVersionId: string; + /** @description The environment ID */ + environmentId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The rollout information */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": (components["schemas"]["ReleaseTarget"] & { + /** Format: date-time */ + rolloutTime: string | null; + rolloutPosition: number; + })[]; + }; + }; + /** @description The deployment version or environment was not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error?: string; + }; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error?: string; + }; + }; + }; + }; + }; updateDeploymentVersion: { parameters: { query?: never; @@ -2756,6 +2804,50 @@ export interface operations { }; }; }; + getPolicy: { + parameters: { + query?: never; + header?: never; + path: { + policyId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Policy"]; + }; + }; + /** @description Policy not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error?: string; + }; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error?: string; + }; + }; + }; + }; + }; deletePolicy: { parameters: { query?: never; @@ -2827,6 +2919,7 @@ export interface operations { requiredApprovalsCount?: number; }[]; concurrency?: components["schemas"]["PolicyConcurrency"]; + environmentVersionRollout?: components["schemas"]["EnvironmentVersionRollout"]; }; }; }; @@ -2940,7 +3033,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["Policy1"]; + "application/json": components["schemas"]["Policy"]; }; }; /** @description Internal Server Error */ diff --git a/e2e/package.json b/e2e/package.json index 6fb113440..757b9c23d 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -39,6 +39,7 @@ "@types/mustache": "^4.2.5", "@types/node": "^22.13.10", "@types/uuid": "^10.0.0", + "date-fns": "catalog:", "eslint": "catalog:", "ms": "catalog:", "openapi-fetch": "^0.13.0", diff --git a/e2e/tests/api/policies/environment-version-rollout-policy.spec.ts b/e2e/tests/api/policies/environment-version-rollout-policy.spec.ts new file mode 100644 index 000000000..c19ddbb09 --- /dev/null +++ b/e2e/tests/api/policies/environment-version-rollout-policy.spec.ts @@ -0,0 +1,623 @@ +import path from "path"; +import { faker } from "@faker-js/faker"; +import { expect } from "@playwright/test"; +import { + addMinutes, + differenceInMilliseconds, + differenceInMinutes, + startOfMinute, +} from "date-fns"; + +import { cleanupImportedEntities, EntitiesBuilder } from "../../../api"; +import { test } from "../../fixtures"; + +const yamlPath = path.join( + __dirname, + "environment-version-rollout-policy.spec.yaml", +); + +test.describe("Environment Version Rollout Policy", () => { + let builder: EntitiesBuilder; + + test.beforeAll(async ({ api, workspace }) => { + builder = new EntitiesBuilder(api, workspace, yamlPath); + + await builder.upsertSystem(); + await builder.upsertResources(); + await builder.upsertEnvironments(); + await builder.upsertDeployments(); + + await new Promise((resolve) => setTimeout(resolve, 1_000)); + }); + + test.afterAll(async ({ api, workspace }) => { + await cleanupImportedEntities(api, builder.refs, workspace.id); + }); + + test("should rollout versions in a linear fashion", async ({ + api, + workspace, + }) => { + const { prefix } = builder.refs; + + const deployment = builder.refs.deployments.find( + (d) => d.name === `${prefix}-linear-rollout`, + )!; + + const environment = builder.refs.environments.find( + (e) => e.name === `${prefix}-linear-rollout`, + )!; + + const timeScaleInterval = faker.number.float({ min: 1, max: 100 }); + const policyResponse = await api.POST("/v1/policies", { + body: { + name: `${prefix}-linear-rollout`, + description: "Linear rollout policy", + priority: 1, + enabled: true, + workspaceId: workspace.id, + targets: [ + { + environmentSelector: { + type: "name", + operator: "equals", + value: `${prefix}-linear-rollout`, + }, + }, + ], + environmentVersionRollout: { + rolloutType: "linear", + timeScaleInterval, + }, + }, + }); + expect(policyResponse.response.status).toBe(200); + expect(policyResponse.data).toBeDefined(); + const policy = policyResponse.data!; + expect(policy.environmentVersionRollout).toBeDefined(); + const environmentVersionRollout = policy.environmentVersionRollout!; + expect(environmentVersionRollout.rolloutType).toBe("linear"); + + const deploymentVersionResponse = await api.POST( + "/v1/deployment-versions", + { + body: { + deploymentId: deployment.id, + tag: faker.string.alphanumeric(10), + }, + }, + ); + + expect(deploymentVersionResponse.response.status).toBe(201); + expect(deploymentVersionResponse.data).toBeDefined(); + const deploymentVersion = deploymentVersionResponse.data!; + + const rolloutResponse = await api.GET( + `/v1/deployment-versions/{deploymentVersionId}/environments/{environmentId}/rollout`, + { + params: { + path: { + deploymentVersionId: deploymentVersion.id, + environmentId: environment.id, + }, + }, + }, + ); + + expect(rolloutResponse.response.status).toBe(200); + const releaseTargetsWithRolloutInfo = rolloutResponse.data ?? []; + expect(releaseTargetsWithRolloutInfo).toHaveLength(4); + + const expectedRolloutStart = startOfMinute( + new Date(deploymentVersion.createdAt), + ); + + for (let i = 0; i < releaseTargetsWithRolloutInfo.length; i++) { + const releaseTarget = releaseTargetsWithRolloutInfo[i]; + const expectedRolloutTime = addMinutes( + expectedRolloutStart, + timeScaleInterval * i, + ); + expect(releaseTarget.rolloutTime).toBeDefined(); + const receivedRolloutTime = new Date(releaseTarget.rolloutTime!); + + const diff = differenceInMilliseconds( + receivedRolloutTime, + expectedRolloutTime, + ); + + expect(diff).toBeLessThan(50); + } + }); + + test("should rollout versions in a linear normalized fashion", async ({ + api, + workspace, + }) => { + const { prefix } = builder.refs; + + const deployment = builder.refs.deployments.find( + (d) => d.name === `${prefix}-linear-rollout-normalized`, + )!; + + const environment = builder.refs.environments.find( + (e) => e.name === `${prefix}-linear-rollout-normalized`, + )!; + + const timeScaleInterval = faker.number.float({ min: 1, max: 100 }); + const policyResponse = await api.POST("/v1/policies", { + body: { + name: `${prefix}-linear-rollout-normalized`, + description: "Linear normalized rollout policy", + priority: 1, + enabled: true, + workspaceId: workspace.id, + targets: [ + { + environmentSelector: { + type: "name", + operator: "equals", + value: `${prefix}-linear-rollout-normalized`, + }, + }, + ], + environmentVersionRollout: { + rolloutType: "linear-normalized", + timeScaleInterval, + }, + }, + }); + expect(policyResponse.response.status).toBe(200); + expect(policyResponse.data).toBeDefined(); + const policy = policyResponse.data!; + expect(policy.environmentVersionRollout).toBeDefined(); + const environmentVersionRollout = policy.environmentVersionRollout!; + expect(environmentVersionRollout.rolloutType).toBe("linear-normalized"); + + const deploymentVersionResponse = await api.POST( + "/v1/deployment-versions", + { + body: { + deploymentId: deployment.id, + tag: faker.string.alphanumeric(10), + }, + }, + ); + + expect(deploymentVersionResponse.response.status).toBe(201); + expect(deploymentVersionResponse.data).toBeDefined(); + const deploymentVersion = deploymentVersionResponse.data!; + + const rolloutResponse = await api.GET( + `/v1/deployment-versions/{deploymentVersionId}/environments/{environmentId}/rollout`, + { + params: { + path: { + deploymentVersionId: deploymentVersion.id, + environmentId: environment.id, + }, + }, + }, + ); + + expect(rolloutResponse.response.status).toBe(200); + const releaseTargetsWithRolloutInfo = rolloutResponse.data ?? []; + expect(releaseTargetsWithRolloutInfo).toHaveLength(4); + + const expectedRolloutStart = startOfMinute( + new Date(deploymentVersion.createdAt), + ); + + for (let i = 0; i < releaseTargetsWithRolloutInfo.length; i++) { + const releaseTarget = releaseTargetsWithRolloutInfo[i]; + const expectedRolloutTime = addMinutes( + expectedRolloutStart, + (timeScaleInterval / releaseTargetsWithRolloutInfo.length) * i, + ); + + expect(releaseTarget.rolloutTime).toBeDefined(); + const receivedRolloutTime = new Date(releaseTarget.rolloutTime!); + + const diff = differenceInMilliseconds( + receivedRolloutTime, + expectedRolloutTime, + ); + + expect(diff).toBeLessThan(50); + } + }); + + test("should rollout versions in a exponential fashion", async ({ + api, + workspace, + }) => { + const { prefix } = builder.refs; + + const deployment = builder.refs.deployments.find( + (d) => d.name === `${prefix}-exponential-rollout`, + )!; + + const environment = builder.refs.environments.find( + (e) => e.name === `${prefix}-exponential-rollout`, + )!; + + const timeScaleInterval = faker.number.float({ min: 1, max: 100 }); + const positionGrowthFactor = faker.number.float({ min: 1, max: 100 }); + const policyResponse = await api.POST("/v1/policies", { + body: { + name: `${prefix}-exponential-rollout`, + description: "Exponential rollout policy", + priority: 1, + enabled: true, + workspaceId: workspace.id, + targets: [ + { + environmentSelector: { + type: "name", + operator: "equals", + value: `${prefix}-exponential-rollout`, + }, + }, + ], + environmentVersionRollout: { + rolloutType: "exponential", + timeScaleInterval, + positionGrowthFactor, + }, + }, + }); + expect(policyResponse.response.status).toBe(200); + expect(policyResponse.data).toBeDefined(); + const policy = policyResponse.data!; + expect(policy.environmentVersionRollout).toBeDefined(); + const environmentVersionRollout = policy.environmentVersionRollout!; + expect(environmentVersionRollout.rolloutType).toBe("exponential"); + + const deploymentVersionResponse = await api.POST( + "/v1/deployment-versions", + { + body: { + deploymentId: deployment.id, + tag: faker.string.alphanumeric(10), + }, + }, + ); + + expect(deploymentVersionResponse.response.status).toBe(201); + expect(deploymentVersionResponse.data).toBeDefined(); + const deploymentVersion = deploymentVersionResponse.data!; + + const rolloutResponse = await api.GET( + `/v1/deployment-versions/{deploymentVersionId}/environments/{environmentId}/rollout`, + { + params: { + path: { + deploymentVersionId: deploymentVersion.id, + environmentId: environment.id, + }, + }, + }, + ); + + expect(rolloutResponse.response.status).toBe(200); + const releaseTargetsWithRolloutInfo = rolloutResponse.data ?? []; + expect(releaseTargetsWithRolloutInfo).toHaveLength(4); + + const expectedRolloutStart = startOfMinute( + new Date(deploymentVersion.createdAt), + ); + + for (let i = 0; i < releaseTargetsWithRolloutInfo.length; i++) { + const releaseTarget = releaseTargetsWithRolloutInfo[i]; + const expectedRolloutTime = addMinutes( + expectedRolloutStart, + timeScaleInterval * (1 - Math.exp(-i / positionGrowthFactor)), + ); + + expect(releaseTarget.rolloutTime).toBeDefined(); + const receivedRolloutTime = new Date(releaseTarget.rolloutTime!); + + const diff = differenceInMilliseconds( + receivedRolloutTime, + expectedRolloutTime, + ); + + expect(diff).toBeLessThan(50); + } + }); + + test("should rollout versions in a exponential normalized fashion", async ({ + api, + workspace, + }) => { + const { prefix } = builder.refs; + + const deployment = builder.refs.deployments.find( + (d) => d.name === `${prefix}-exponential-rollout-normalized`, + )!; + + const environment = builder.refs.environments.find( + (e) => e.name === `${prefix}-exponential-rollout-normalized`, + )!; + + const timeScaleInterval = faker.number.float({ min: 1, max: 100 }); + const positionGrowthFactor = faker.number.float({ min: 1, max: 100 }); + const policyResponse = await api.POST("/v1/policies", { + body: { + name: `${prefix}-exponential-rollout-normalized`, + description: "Exponential normalized rollout policy", + priority: 1, + enabled: true, + workspaceId: workspace.id, + targets: [ + { + environmentSelector: { + type: "name", + operator: "equals", + value: `${prefix}-exponential-rollout-normalized`, + }, + }, + ], + environmentVersionRollout: { + rolloutType: "exponential-normalized", + timeScaleInterval, + positionGrowthFactor, + }, + }, + }); + expect(policyResponse.response.status).toBe(200); + expect(policyResponse.data).toBeDefined(); + const policy = policyResponse.data!; + expect(policy.environmentVersionRollout).toBeDefined(); + const environmentVersionRollout = policy.environmentVersionRollout!; + expect(environmentVersionRollout.rolloutType).toBe( + "exponential-normalized", + ); + + const deploymentVersionResponse = await api.POST( + "/v1/deployment-versions", + { + body: { + deploymentId: deployment.id, + tag: faker.string.alphanumeric(10), + }, + }, + ); + + expect(deploymentVersionResponse.response.status).toBe(201); + expect(deploymentVersionResponse.data).toBeDefined(); + const deploymentVersion = deploymentVersionResponse.data!; + + const rolloutResponse = await api.GET( + `/v1/deployment-versions/{deploymentVersionId}/environments/{environmentId}/rollout`, + { + params: { + path: { + deploymentVersionId: deploymentVersion.id, + environmentId: environment.id, + }, + }, + }, + ); + + expect(rolloutResponse.response.status).toBe(200); + const releaseTargetsWithRolloutInfo = rolloutResponse.data ?? []; + expect(releaseTargetsWithRolloutInfo).toHaveLength(4); + + const expectedRolloutStart = startOfMinute( + new Date(deploymentVersion.createdAt), + ); + + for (let i = 0; i < releaseTargetsWithRolloutInfo.length; i++) { + const releaseTarget = releaseTargetsWithRolloutInfo[i]; + const numReleaseTargets = releaseTargetsWithRolloutInfo.length; + const expectedRolloutTime = addMinutes( + expectedRolloutStart, + timeScaleInterval * + ((1 - Math.exp(-i / numReleaseTargets)) / + (1 - Math.exp(-numReleaseTargets / positionGrowthFactor))), + ); + + expect(releaseTarget.rolloutTime).toBeDefined(); + const receivedRolloutTime = new Date(releaseTarget.rolloutTime!); + + const diff = differenceInMilliseconds( + receivedRolloutTime, + expectedRolloutTime, + ); + + expect(diff).toBeLessThan(50); + } + }); + + test("rollout times should be null if policy requires approvals and the deployment version has no approvals", async ({ + api, + workspace, + }) => { + const { prefix } = builder.refs; + + const deployment = builder.refs.deployments.find( + (d) => d.name === `${prefix}-no-approvals`, + )!; + + const environment = builder.refs.environments.find( + (e) => e.name === `${prefix}-no-approvals`, + )!; + + const timeScaleInterval = faker.number.float({ min: 1, max: 100 }); + const policyResponse = await api.POST("/v1/policies", { + body: { + name: `${prefix}-no-approvals`, + description: "No approvals rollout policy", + priority: 1, + enabled: true, + workspaceId: workspace.id, + targets: [ + { + environmentSelector: { + type: "name", + operator: "equals", + value: `${prefix}-no-approvals`, + }, + }, + ], + environmentVersionRollout: { + rolloutType: "linear", + timeScaleInterval, + }, + versionAnyApprovals: { requiredApprovalsCount: 1 }, + }, + }); + expect(policyResponse.response.status).toBe(200); + expect(policyResponse.data).toBeDefined(); + const policy = policyResponse.data!; + expect(policy.environmentVersionRollout).toBeDefined(); + const environmentVersionRollout = policy.environmentVersionRollout!; + expect(environmentVersionRollout.rolloutType).toBe("linear"); + + const deploymentVersionResponse = await api.POST( + "/v1/deployment-versions", + { + body: { + deploymentId: deployment.id, + tag: faker.string.alphanumeric(10), + }, + }, + ); + + expect(deploymentVersionResponse.response.status).toBe(201); + expect(deploymentVersionResponse.data).toBeDefined(); + const deploymentVersion = deploymentVersionResponse.data!; + + const rolloutResponse = await api.GET( + `/v1/deployment-versions/{deploymentVersionId}/environments/{environmentId}/rollout`, + { + params: { + path: { + deploymentVersionId: deploymentVersion.id, + environmentId: environment.id, + }, + }, + }, + ); + + expect(rolloutResponse.response.status).toBe(200); + const releaseTargetsWithRolloutInfo = rolloutResponse.data ?? []; + expect(releaseTargetsWithRolloutInfo).toHaveLength(4); + + for (let i = 0; i < releaseTargetsWithRolloutInfo.length; i++) { + const releaseTarget = releaseTargetsWithRolloutInfo[i]; + expect(releaseTarget.rolloutTime).toBeNull(); + } + }); + + test("should rollout versions in a linear fashion with start time being the latest approval time", async ({ + api, + workspace, + }) => { + const { prefix } = builder.refs; + + const deployment = builder.refs.deployments.find( + (d) => d.name === `${prefix}-linear-rollout-with-approvals`, + )!; + + const environment = builder.refs.environments.find( + (e) => e.name === `${prefix}-linear-rollout-with-approvals`, + )!; + + const timeScaleInterval = faker.number.float({ min: 1, max: 100 }); + const policyResponse = await api.POST("/v1/policies", { + body: { + name: `${prefix}-linear-rollout-with-approvals`, + description: "Linear rollout policy with approvals", + priority: 1, + enabled: true, + workspaceId: workspace.id, + targets: [ + { + environmentSelector: { + type: "name", + operator: "equals", + value: `${prefix}-linear-rollout-with-approvals`, + }, + }, + ], + environmentVersionRollout: { + rolloutType: "linear", + timeScaleInterval, + }, + versionAnyApprovals: { requiredApprovalsCount: 1 }, + }, + }); + expect(policyResponse.response.status).toBe(200); + expect(policyResponse.data).toBeDefined(); + const policy = policyResponse.data!; + expect(policy.environmentVersionRollout).toBeDefined(); + const environmentVersionRollout = policy.environmentVersionRollout!; + expect(environmentVersionRollout.rolloutType).toBe("linear"); + + const deploymentVersionResponse = await api.POST( + "/v1/deployment-versions", + { + body: { + deploymentId: deployment.id, + tag: faker.string.alphanumeric(10), + }, + }, + ); + + expect(deploymentVersionResponse.response.status).toBe(201); + expect(deploymentVersionResponse.data).toBeDefined(); + const deploymentVersion = deploymentVersionResponse.data!; + + const tenMinutesFromNow = addMinutes(new Date(), 10); + const approvalResponse = await api.POST( + "/v1/deployment-versions/{deploymentVersionId}/approve", + { + params: { + path: { + deploymentVersionId: deploymentVersion.id, + }, + }, + body: { approvedAt: tenMinutesFromNow.toISOString() }, + }, + ); + expect(approvalResponse.response.status).toBe(200); + + const rolloutResponse = await api.GET( + `/v1/deployment-versions/{deploymentVersionId}/environments/{environmentId}/rollout`, + { + params: { + path: { + deploymentVersionId: deploymentVersion.id, + environmentId: environment.id, + }, + }, + }, + ); + + expect(rolloutResponse.response.status).toBe(200); + const releaseTargetsWithRolloutInfo = rolloutResponse.data ?? []; + expect(releaseTargetsWithRolloutInfo).toHaveLength(4); + + const expectedRolloutStart = startOfMinute(tenMinutesFromNow); + + for (let i = 0; i < releaseTargetsWithRolloutInfo.length; i++) { + const releaseTarget = releaseTargetsWithRolloutInfo[i]; + const expectedRolloutTime = addMinutes( + expectedRolloutStart, + timeScaleInterval * i, + ); + + expect(releaseTarget.rolloutTime).toBeDefined(); + const receivedRolloutTime = new Date(releaseTarget.rolloutTime!); + + const diff = differenceInMilliseconds( + receivedRolloutTime, + expectedRolloutTime, + ); + + expect(diff).toBeLessThan(50); + } + }); +}); diff --git a/e2e/tests/api/policies/environment-version-rollout-policy.spec.yaml b/e2e/tests/api/policies/environment-version-rollout-policy.spec.yaml new file mode 100644 index 000000000..0fc292d98 --- /dev/null +++ b/e2e/tests/api/policies/environment-version-rollout-policy.spec.yaml @@ -0,0 +1,104 @@ +system: + name: "{{ prefix }}-environment-version-rollout-policy-tester" + slug: "{{ prefix }}-environment-version-rollout-policy-tester" + description: System for testing environment version rollout policy + +deployments: + - name: "{{ prefix }}-linear-rollout" + slug: "{{ prefix }}-linear-rollout" + - name: "{{ prefix }}-linear-rollout-normalized" + slug: "{{ prefix }}-linear-rollout-normalized" + - name: "{{ prefix }}-exponential-rollout" + slug: "{{ prefix }}-exponential-rollout" + - name: "{{ prefix }}-exponential-rollout-normalized" + slug: "{{ prefix }}-exponential-rollout-normalized" + - name: "{{ prefix }}-no-approvals" + slug: "{{ prefix }}-no-approvals" + - name: "{{ prefix }}-linear-rollout-with-approvals" + slug: "{{ prefix }}-linear-rollout-with-approvals" + +environments: + - name: "{{ prefix }}-linear-rollout" + description: Environment for testing environment version rollout policy + systemId: "{{ prefix }}-environment-version-rollout-policy-tester" + resourceSelector: + type: identifier + operator: contains + value: "{{ prefix }}" + + - name: "{{ prefix }}-linear-rollout-normalized" + description: Environment for testing environment version rollout policy with normalized linear rollout + systemId: "{{ prefix }}-environment-version-rollout-policy-tester" + resourceSelector: + type: identifier + operator: contains + value: "{{ prefix }}" + + - name: "{{ prefix }}-exponential-rollout" + description: Environment for testing environment version rollout policy with exponential rollout + systemId: "{{ prefix }}-environment-version-rollout-policy-tester" + resourceSelector: + type: identifier + operator: contains + value: "{{ prefix }}" + + - name: "{{ prefix }}-exponential-rollout-normalized" + description: Environment for testing environment version rollout policy with normalized exponential rollout + systemId: "{{ prefix }}-environment-version-rollout-policy-tester" + resourceSelector: + type: identifier + operator: contains + value: "{{ prefix }}" + + - name: "{{ prefix }}-no-approvals" + description: Environment for testing environment version rollout policy with no approvals + systemId: "{{ prefix }}-environment-version-rollout-policy-tester" + resourceSelector: + type: identifier + operator: contains + value: "{{ prefix }}" + + - name: "{{ prefix }}-linear-rollout-with-approvals" + description: Environment for testing environment version rollout policy with linear rollout and approvals + systemId: "{{ prefix }}-environment-version-rollout-policy-tester" + resourceSelector: + type: identifier + operator: contains + value: "{{ prefix }}" + +resources: + - name: "{{ prefix }}-a" + kind: service + identifier: "{{ prefix }}-a" + version: 1.0.0 + config: + enabled: true + metadata: + env: prod + + - name: "{{ prefix }}-b" + kind: service + identifier: "{{ prefix }}-b" + version: 1.0.0 + config: + enabled: true + metadata: + env: prod + + - name: "{{ prefix }}-c" + kind: service + identifier: "{{ prefix }}-c" + version: 1.0.0 + config: + enabled: true + metadata: + env: prod + + - name: "{{ prefix }}-d" + kind: service + identifier: "{{ prefix }}-d" + version: 1.0.0 + config: + enabled: true + metadata: + env: prod diff --git a/openapi.v1.json b/openapi.v1.json index cfec7e96f..dc4eb3ea6 100644 --- a/openapi.v1.json +++ b/openapi.v1.json @@ -244,6 +244,11 @@ "properties": { "reason": { "type": "string" + }, + "approvedAt": { + "type": "string", + "format": "date-time", + "nullable": true } } } @@ -309,6 +314,100 @@ } } }, + "/v1/deployment-versions/{deploymentVersionId}/environments/{environmentId}/rollout": { + "get": { + "summary": "Get the rollout information across all release targets for a given deployment version and environment", + "operationId": "getRolloutInfo", + "parameters": [ + { + "name": "deploymentVersionId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "The deployment version ID" + }, + { + "name": "environmentId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "The environment ID" + } + ], + "responses": { + "200": { + "description": "The rollout information", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/ReleaseTarget" + }, + { + "type": "object", + "properties": { + "rolloutTime": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "rolloutPosition": { + "type": "number" + } + }, + "required": [ + "rolloutTime", + "rolloutPosition" + ] + } + ] + } + } + } + } + }, + "404": { + "description": "The deployment version or environment was 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" + } + } + } + } + } + } + } + } + }, "/v1/deployment-versions/{deploymentVersionId}": { "patch": { "summary": "Updates a deployment version", @@ -1999,6 +2098,63 @@ } }, "/v1/policies/{policyId}": { + "get": { + "summary": "Get a policy", + "operationId": "getPolicy", + "parameters": [ + { + "name": "policyId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Policy" + } + } + } + }, + "404": { + "description": "Policy 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" + } + } + } + } + } + } + } + }, "patch": { "summary": "Update a policy", "operationId": "updatePolicy", @@ -2102,6 +2258,9 @@ }, "concurrency": { "$ref": "#/components/schemas/PolicyConcurrency" + }, + "environmentVersionRollout": { + "$ref": "#/components/schemas/EnvironmentVersionRollout" } } } @@ -2345,7 +2504,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Policy1" + "$ref": "#/components/schemas/Policy" } } } @@ -5528,83 +5687,6 @@ "jobAgentConfig" ] }, - "Policy": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uuid", - "description": "The policy ID" - }, - "systemId": { - "type": "string", - "format": "uuid", - "description": "The system ID" - }, - "name": { - "type": "string", - "description": "The name of the policy" - }, - "description": { - "type": "string", - "nullable": true, - "description": "The description of the policy" - }, - "approvalRequirement": { - "type": "string", - "enum": [ - "manual", - "automatic" - ], - "description": "The approval requirement of the policy" - }, - "successType": { - "type": "string", - "enum": [ - "some", - "all", - "optional" - ], - "description": "If a policy depends on an environment, whether or not the policy requires all, some, or optional successful releases in the environment" - }, - "successMinimum": { - "type": "number", - "description": "If a policy depends on an environment, the minimum number of successful releases in the environment" - }, - "concurrencyLimit": { - "type": "number", - "nullable": true, - "description": "The maximum number of concurrent releases in the environment" - }, - "rolloutDuration": { - "type": "number", - "description": "The duration of the rollout in milliseconds" - }, - "minimumReleaseInterval": { - "type": "number", - "description": "The minimum interval between releases in milliseconds" - }, - "releaseSequencing": { - "type": "string", - "enum": [ - "wait", - "cancel" - ], - "description": "If a new release is created, whether it will wait for the current release to finish before starting, or cancel the current release" - } - }, - "required": [ - "id", - "systemId", - "name", - "approvalRequirement", - "successType", - "successMinimum", - "rolloutDuration", - "minimumReleaseInterval", - "releaseSequencing" - ] - }, "Environment": { "type": "object", "properties": { @@ -6117,7 +6199,35 @@ "minimum": 1, "format": "int32" }, - "Policy1": { + "EnvironmentVersionRollout": { + "type": "object", + "properties": { + "positionGrowthFactor": { + "type": "number", + "description": "Controls how strongly queue position influences delay — higher values result in a smoother, slower rollout curve." + }, + "timeScaleInterval": { + "type": "number", + "description": "Defines the base time interval that each unit of rollout progression is scaled by — larger values stretch the deployment timeline." + }, + "rolloutType": { + "type": "string", + "enum": [ + "linear", + "linear-normalized", + "exponential", + "exponential-normalized" + ], + "description": "Determines the shape of the rollout curve — linear, exponential, or normalized versions of each. A normalized rollout curve limits the maximum delay to the time scale interval, and scales the rollout progression to fit within that interval." + } + }, + "required": [ + "positionGrowthFactor", + "timeScaleInterval", + "rolloutType" + ] + }, + "Policy": { "type": "object", "properties": { "id": { @@ -6176,6 +6286,9 @@ }, "concurrency": { "$ref": "#/components/schemas/PolicyConcurrency" + }, + "environmentVersionRollout": { + "$ref": "#/components/schemas/EnvironmentVersionRollout" } }, "required": [ diff --git a/packages/api/src/router/policy/evaluate.ts b/packages/api/src/router/policy/evaluate.ts index 8a969e001..2729bb500 100644 --- a/packages/api/src/router/policy/evaluate.ts +++ b/packages/api/src/router/policy/evaluate.ts @@ -4,66 +4,18 @@ import { TRPCError } from "@trpc/server"; import { isPresent } from "ts-is-present"; import { z } from "zod"; -import { and, eq, inArray, isNull, selector } from "@ctrlplane/db"; +import { and, eq, selector } from "@ctrlplane/db"; import * as schema from "@ctrlplane/db/schema"; import { versionAnyApprovalRule, versionRoleApprovalRule, versionUserApprovalRule, } from "@ctrlplane/rule-engine"; +import { getApplicablePoliciesWithoutResourceScope } from "@ctrlplane/rule-engine/db"; import { Permission } from "@ctrlplane/validators/auth"; import { protectedProcedure } from "../../trpc"; -const getApplicablePoliciesWithoutResourceScope = async ( - db: Tx, - environmentId: string, - deploymentId: string, -) => { - const policyIdResults = await db - .selectDistinct({ policyId: schema.policy.id }) - .from(schema.computedPolicyTargetReleaseTarget) - .innerJoin( - schema.policyTarget, - eq( - schema.computedPolicyTargetReleaseTarget.policyTargetId, - schema.policyTarget.id, - ), - ) - .innerJoin( - schema.policy, - eq(schema.policyTarget.policyId, schema.policy.id), - ) - .innerJoin( - schema.releaseTarget, - eq( - schema.computedPolicyTargetReleaseTarget.releaseTargetId, - schema.releaseTarget.id, - ), - ) - .where( - and( - isNull(schema.policyTarget.resourceSelector), - eq(schema.releaseTarget.environmentId, environmentId), - eq(schema.releaseTarget.deploymentId, deploymentId), - eq(schema.policy.enabled, true), - ), - ); - - const policyIds = policyIdResults.map((r) => r.policyId); - return db.query.policy.findMany({ - where: inArray(schema.policy.id, policyIds), - with: { - denyWindows: true, - deploymentVersionSelector: true, - versionAnyApprovals: true, - versionRoleApprovals: true, - versionUserApprovals: true, - concurrency: true, - }, - }); -}; - /** * Evaluates whether a version matches a policy's version selector rules. * This is used to determine if a version is allowed to be deployed based on diff --git a/packages/api/src/router/policy/router.ts b/packages/api/src/router/policy/router.ts index 95aa07632..a61bffaa5 100644 --- a/packages/api/src/router/policy/router.ts +++ b/packages/api/src/router/policy/router.ts @@ -80,6 +80,7 @@ export const policyRouter = createTRPCRouter({ versionUserApprovals: true, versionRoleApprovals: true, concurrency: true, + environmentVersionRollout: true, }, }), ), diff --git a/packages/db/drizzle/0112_steady_proteus.sql b/packages/db/drizzle/0112_steady_proteus.sql new file mode 100644 index 000000000..79e7d5852 --- /dev/null +++ b/packages/db/drizzle/0112_steady_proteus.sql @@ -0,0 +1,12 @@ +CREATE TYPE "public"."rollout_type" AS ENUM('linear', 'linear_normalized', 'exponential', 'exponential_normalized');--> statement-breakpoint +CREATE TABLE "policy_rule_environment_version_rollout" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "policy_id" uuid NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "position_growth_factor" real DEFAULT 1 NOT NULL, + "time_scale_interval" real NOT NULL, + "rollout_type" "rollout_type" DEFAULT 'linear' NOT NULL, + CONSTRAINT "policy_rule_environment_version_rollout_policy_id_unique" UNIQUE("policy_id") +); +--> statement-breakpoint +ALTER TABLE "policy_rule_environment_version_rollout" ADD CONSTRAINT "policy_rule_environment_version_rollout_policy_id_policy_id_fk" FOREIGN KEY ("policy_id") REFERENCES "public"."policy"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/packages/db/drizzle/meta/0112_snapshot.json b/packages/db/drizzle/meta/0112_snapshot.json new file mode 100644 index 000000000..952ffb576 --- /dev/null +++ b/packages/db/drizzle/meta/0112_snapshot.json @@ -0,0 +1,6612 @@ +{ + "id": "a50794f5-6ce6-4e7c-af83-f54110cbbcf2", + "prevId": "4fb51637-b3f6-4e2d-a206-b0b4232ae9c0", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "name": "account_provider_providerAccountId_pk", + "columns": ["provider", "providerAccountId"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "active_workspace_id": { + "name": "active_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "system_role": { + "name": "system_role", + "type": "system_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_active_workspace_id_workspace_id_fk": { + "name": "user_active_workspace_id_workspace_id_fk", + "tableFrom": "user", + "tableTo": "workspace", + "columnsFrom": ["active_workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_api_key": { + "name": "user_api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key_preview": { + "name": "key_preview", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_prefix": { + "name": "key_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_api_key_key_prefix_key_hash_index": { + "name": "user_api_key_key_prefix_key_hash_index", + "columns": [ + { + "expression": "key_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_api_key_user_id_user_id_fk": { + "name": "user_api_key_user_id_user_id_fk", + "tableFrom": "user_api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard": { + "name": "dashboard", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_workspace_id_workspace_id_fk": { + "name": "dashboard_workspace_id_workspace_id_fk", + "tableFrom": "dashboard", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_widget": { + "name": "dashboard_widget", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "dashboard_id": { + "name": "dashboard_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "widget": { + "name": "widget", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "x": { + "name": "x", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "y": { + "name": "y", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "w": { + "name": "w", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "h": { + "name": "h", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_widget_dashboard_id_dashboard_id_fk": { + "name": "dashboard_widget_dashboard_id_dashboard_id_fk", + "tableFrom": "dashboard_widget", + "tableTo": "dashboard", + "columnsFrom": ["dashboard_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_variable": { + "name": "deployment_variable", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "default_value_id": { + "name": "default_value_id", + "type": "uuid", + "primaryKey": false, + "notNull": false, + "default": "NULL" + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "deployment_variable_deployment_id_key_index": { + "name": "deployment_variable_deployment_id_key_index", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_variable_deployment_id_deployment_id_fk": { + "name": "deployment_variable_deployment_id_deployment_id_fk", + "tableFrom": "deployment_variable", + "tableTo": "deployment", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_variable_default_value_id_deployment_variable_value_id_fk": { + "name": "deployment_variable_default_value_id_deployment_variable_value_id_fk", + "tableFrom": "deployment_variable", + "tableTo": "deployment_variable_value", + "columnsFrom": ["default_value_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_variable_set": { + "name": "deployment_variable_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "variable_set_id": { + "name": "variable_set_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "deployment_variable_set_deployment_id_variable_set_id_index": { + "name": "deployment_variable_set_deployment_id_variable_set_id_index", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "variable_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_variable_set_deployment_id_deployment_id_fk": { + "name": "deployment_variable_set_deployment_id_deployment_id_fk", + "tableFrom": "deployment_variable_set", + "tableTo": "deployment", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_variable_set_variable_set_id_variable_set_id_fk": { + "name": "deployment_variable_set_variable_set_id_variable_set_id_fk", + "tableFrom": "deployment_variable_set", + "tableTo": "variable_set", + "columnsFrom": ["variable_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_variable_value": { + "name": "deployment_variable_value", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "variable_id": { + "name": "variable_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_selector": { + "name": "resource_selector", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "NULL" + } + }, + "indexes": {}, + "foreignKeys": { + "deployment_variable_value_variable_id_deployment_variable_id_fk": { + "name": "deployment_variable_value_variable_id_deployment_variable_id_fk", + "tableFrom": "deployment_variable_value", + "tableTo": "deployment_variable", + "columnsFrom": ["variable_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "restrict" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_variable_value_direct": { + "name": "deployment_variable_value_direct", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "variable_value_id": { + "name": "variable_value_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "value_hash": { + "name": "value_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "deployment_variable_value_direct_variable_value_id_deployment_variable_value_id_fk": { + "name": "deployment_variable_value_direct_variable_value_id_deployment_variable_value_id_fk", + "tableFrom": "deployment_variable_value_direct", + "tableTo": "deployment_variable_value", + "columnsFrom": ["variable_value_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "deployment_variable_value_direct_variable_value_id_unique": { + "name": "deployment_variable_value_direct_variable_value_id_unique", + "nullsNotDistinct": false, + "columns": ["variable_value_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_variable_value_reference": { + "name": "deployment_variable_value_reference", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "variable_value_id": { + "name": "variable_value_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "reference": { + "name": "reference", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "default_value": { + "name": "default_value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "deployment_variable_value_reference_variable_value_id_deployment_variable_value_id_fk": { + "name": "deployment_variable_value_reference_variable_value_id_deployment_variable_value_id_fk", + "tableFrom": "deployment_variable_value_reference", + "tableTo": "deployment_variable_value", + "columnsFrom": ["variable_value_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "deployment_variable_value_reference_variable_value_id_unique": { + "name": "deployment_variable_value_reference_variable_value_id_unique", + "nullsNotDistinct": false, + "columns": ["variable_value_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_version": { + "name": "deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "job_agent_config": { + "name": "job_agent_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "deployment_version_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ready'" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "deployment_version_deployment_id_tag_index": { + "name": "deployment_version_deployment_id_tag_index", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_version_created_at_idx": { + "name": "deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_version_deployment_id_deployment_id_fk": { + "name": "deployment_version_deployment_id_deployment_id_fk", + "tableFrom": "deployment_version", + "tableTo": "deployment", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_version_channel": { + "name": "deployment_version_channel", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "deployment_version_selector": { + "name": "deployment_version_selector", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "NULL" + } + }, + "indexes": { + "deployment_version_channel_deployment_id_name_index": { + "name": "deployment_version_channel_deployment_id_name_index", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_version_channel_deployment_id_deployment_id_fk": { + "name": "deployment_version_channel_deployment_id_deployment_id_fk", + "tableFrom": "deployment_version_channel", + "tableTo": "deployment", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_version_metadata": { + "name": "deployment_version_metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "deployment_version_metadata_key_deployment_version_id_index": { + "name": "deployment_version_metadata_key_deployment_version_id_index", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_version_metadata_version_id_idx": { + "name": "deployment_version_metadata_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_version_metadata_deployment_version_id_deployment_version_id_fk": { + "name": "deployment_version_metadata_deployment_version_id_deployment_version_id_fk", + "tableFrom": "deployment_version_metadata", + "tableTo": "deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_version_dependency": { + "name": "deployment_version_dependency", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "deployment_version_selector": { + "name": "deployment_version_selector", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "NULL" + } + }, + "indexes": { + "deployment_version_dependency_deployment_version_id_deployment_id_index": { + "name": "deployment_version_dependency_deployment_version_id_deployment_id_index", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_version_dependency_deployment_version_id_deployment_version_id_fk": { + "name": "deployment_version_dependency_deployment_version_id_deployment_version_id_fk", + "tableFrom": "deployment_version_dependency", + "tableTo": "deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_version_dependency_deployment_id_deployment_id_fk": { + "name": "deployment_version_dependency_deployment_id_deployment_id_fk", + "tableFrom": "deployment_version_dependency", + "tableTo": "deployment", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.computed_deployment_resource": { + "name": "computed_deployment_resource", + "schema": "", + "columns": { + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "computed_deployment_resource_deployment_id_deployment_id_fk": { + "name": "computed_deployment_resource_deployment_id_deployment_id_fk", + "tableFrom": "computed_deployment_resource", + "tableTo": "deployment", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "computed_deployment_resource_resource_id_resource_id_fk": { + "name": "computed_deployment_resource_resource_id_resource_id_fk", + "tableFrom": "computed_deployment_resource", + "tableTo": "resource", + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "computed_deployment_resource_deployment_id_resource_id_pk": { + "name": "computed_deployment_resource_deployment_id_resource_id_pk", + "columns": ["deployment_id", "resource_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment": { + "name": "deployment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "system_id": { + "name": "system_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_agent_id": { + "name": "job_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "job_agent_config": { + "name": "job_agent_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": "NULL" + }, + "resource_selector": { + "name": "resource_selector", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "NULL" + } + }, + "indexes": { + "deployment_system_id_slug_index": { + "name": "deployment_system_id_slug_index", + "columns": [ + { + "expression": "system_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_system_id_system_id_fk": { + "name": "deployment_system_id_system_id_fk", + "tableFrom": "deployment", + "tableTo": "system", + "columnsFrom": ["system_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_job_agent_id_job_agent_id_fk": { + "name": "deployment_job_agent_id_job_agent_id_fk", + "tableFrom": "deployment", + "tableTo": "job_agent", + "columnsFrom": ["job_agent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_policy_deployment": { + "name": "environment_policy_deployment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "environment_policy_deployment_policy_id_environment_id_index": { + "name": "environment_policy_deployment_policy_id_environment_id_index", + "columns": [ + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environment_policy_deployment_policy_id_environment_policy_id_fk": { + "name": "environment_policy_deployment_policy_id_environment_policy_id_fk", + "tableFrom": "environment_policy_deployment", + "tableTo": "environment_policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_policy_deployment_environment_id_environment_id_fk": { + "name": "environment_policy_deployment_environment_id_environment_id_fk", + "tableFrom": "environment_policy_deployment", + "tableTo": "environment", + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.computed_environment_resource": { + "name": "computed_environment_resource", + "schema": "", + "columns": { + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "computed_environment_resource_environment_id_environment_id_fk": { + "name": "computed_environment_resource_environment_id_environment_id_fk", + "tableFrom": "computed_environment_resource", + "tableTo": "environment", + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "computed_environment_resource_resource_id_resource_id_fk": { + "name": "computed_environment_resource_resource_id_resource_id_fk", + "tableFrom": "computed_environment_resource", + "tableTo": "resource", + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "computed_environment_resource_environment_id_resource_id_pk": { + "name": "computed_environment_resource_environment_id_resource_id_pk", + "columns": ["environment_id", "resource_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "system_id": { + "name": "system_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "directory": { + "name": "directory", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_selector": { + "name": "resource_selector", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "NULL" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "environment_system_id_name_index": { + "name": "environment_system_id_name_index", + "columns": [ + { + "expression": "system_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environment_system_id_system_id_fk": { + "name": "environment_system_id_system_id_fk", + "tableFrom": "environment", + "tableTo": "system", + "columnsFrom": ["system_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_policy_id_environment_policy_id_fk": { + "name": "environment_policy_id_environment_policy_id_fk", + "tableFrom": "environment", + "tableTo": "environment_policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_metadata": { + "name": "environment_metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "environment_metadata_key_environment_id_index": { + "name": "environment_metadata_key_environment_id_index", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environment_metadata_environment_id_environment_id_fk": { + "name": "environment_metadata_environment_id_environment_id_fk", + "tableFrom": "environment_metadata", + "tableTo": "environment", + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_policy": { + "name": "environment_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_id": { + "name": "system_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approval_required": { + "name": "approval_required", + "type": "environment_policy_approval_requirement", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'automatic'" + }, + "success_status": { + "name": "success_status", + "type": "environment_policy_deployment_success_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "minimum_success": { + "name": "minimum_success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "concurrency_limit": { + "name": "concurrency_limit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": "NULL" + }, + "rollout_duration": { + "name": "rollout_duration", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "minimum_release_interval": { + "name": "minimum_release_interval", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "release_sequencing": { + "name": "release_sequencing", + "type": "release_sequencing_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'cancel'" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_policy_system_id_system_id_fk": { + "name": "environment_policy_system_id_system_id_fk", + "tableFrom": "environment_policy", + "tableTo": "system", + "columnsFrom": ["system_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_policy_environment_id_environment_id_fk": { + "name": "environment_policy_environment_id_environment_id_fk", + "tableFrom": "environment_policy", + "tableTo": "environment", + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_policy_approval": { + "name": "environment_policy_approval", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "release_id": { + "name": "release_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "approval_status_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp (0) with time zone", + "primaryKey": false, + "notNull": false, + "default": "NULL" + } + }, + "indexes": { + "environment_policy_approval_policy_id_release_id_index": { + "name": "environment_policy_approval_policy_id_release_id_index", + "columns": [ + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environment_policy_approval_policy_id_environment_policy_id_fk": { + "name": "environment_policy_approval_policy_id_environment_policy_id_fk", + "tableFrom": "environment_policy_approval", + "tableTo": "environment_policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_policy_approval_release_id_deployment_version_id_fk": { + "name": "environment_policy_approval_release_id_deployment_version_id_fk", + "tableFrom": "environment_policy_approval", + "tableTo": "deployment_version", + "columnsFrom": ["release_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_policy_approval_user_id_user_id_fk": { + "name": "environment_policy_approval_user_id_user_id_fk", + "tableFrom": "environment_policy_approval", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_policy_release_window": { + "name": "environment_policy_release_window", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "start_time": { + "name": "start_time", + "type": "timestamp (0) with time zone", + "primaryKey": false, + "notNull": true + }, + "end_time": { + "name": "end_time", + "type": "timestamp (0) with time zone", + "primaryKey": false, + "notNull": true + }, + "recurrence": { + "name": "recurrence", + "type": "recurrence_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "environment_policy_release_window_policy_id_environment_policy_id_fk": { + "name": "environment_policy_release_window_policy_id_environment_policy_id_fk", + "tableFrom": "environment_policy_release_window", + "tableTo": "environment_policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.event": { + "name": "event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "event_workspace_id_workspace_id_fk": { + "name": "event_workspace_id_workspace_id_fk", + "tableFrom": "event", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hook": { + "name": "hook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "name_scope_type_scope_id_unique": { + "name": "name_scope_type_scope_id_unique", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.runhook": { + "name": "runhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "hook_id": { + "name": "hook_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "runbook_id": { + "name": "runbook_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "runhook_hook_id_runbook_id_index": { + "name": "runhook_hook_id_runbook_id_index", + "columns": [ + { + "expression": "hook_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "runbook_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "runhook_hook_id_hook_id_fk": { + "name": "runhook_hook_id_hook_id_fk", + "tableFrom": "runhook", + "tableTo": "hook", + "columnsFrom": ["hook_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "runhook_runbook_id_runbook_id_fk": { + "name": "runhook_runbook_id_runbook_id_fk", + "tableFrom": "runhook", + "tableTo": "runbook", + "columnsFrom": ["runbook_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_entity": { + "name": "github_entity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "installation_id": { + "name": "installation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "github_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "added_by_user_id": { + "name": "added_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_installation_workspace": { + "name": "unique_installation_workspace", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_entity_added_by_user_id_user_id_fk": { + "name": "github_entity_added_by_user_id_user_id_fk", + "tableFrom": "github_entity", + "tableTo": "user", + "columnsFrom": ["added_by_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "github_entity_workspace_id_workspace_id_fk": { + "name": "github_entity_workspace_id_workspace_id_fk", + "tableFrom": "github_entity", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_user": { + "name": "github_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "github_user_id": { + "name": "github_user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "github_username": { + "name": "github_username", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "github_user_user_id_user_id_fk": { + "name": "github_user_user_id_user_id_fk", + "tableFrom": "github_user", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_provider_github_repo": { + "name": "resource_provider_github_repo", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "resource_provider_id": { + "name": "resource_provider_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "github_entity_id": { + "name": "github_entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "repo_id": { + "name": "repo_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "unique_resource_provider_github_entity_repo": { + "name": "unique_resource_provider_github_entity_repo", + "columns": [ + { + "expression": "github_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_provider_github_repo_resource_provider_id_resource_provider_id_fk": { + "name": "resource_provider_github_repo_resource_provider_id_resource_provider_id_fk", + "tableFrom": "resource_provider_github_repo", + "tableTo": "resource_provider", + "columnsFrom": ["resource_provider_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resource_provider_github_repo_github_entity_id_github_entity_id_fk": { + "name": "resource_provider_github_repo_github_entity_id_github_entity_id_fk", + "tableFrom": "resource_provider_github_repo", + "tableTo": "github_entity", + "columnsFrom": ["github_entity_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_resource_relationship": { + "name": "job_resource_relationship", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_identifier": { + "name": "resource_identifier", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "job_resource_relationship_job_id_resource_identifier_index": { + "name": "job_resource_relationship_job_id_resource_identifier_index", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_resource_relationship_job_id_job_id_fk": { + "name": "job_resource_relationship_job_id_job_id_fk", + "tableFrom": "job_resource_relationship", + "tableTo": "job", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource": { + "name": "resource", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resource_identifier_workspace_id_index": { + "name": "resource_identifier_workspace_id_index", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_provider_id_resource_provider_id_fk": { + "name": "resource_provider_id_resource_provider_id_fk", + "tableFrom": "resource", + "tableTo": "resource_provider", + "columnsFrom": ["provider_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "resource_workspace_id_workspace_id_fk": { + "name": "resource_workspace_id_workspace_id_fk", + "tableFrom": "resource", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_metadata": { + "name": "resource_metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "resource_metadata_key_resource_id_index": { + "name": "resource_metadata_key_resource_id_index", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_metadata_resource_id_resource_id_fk": { + "name": "resource_metadata_resource_id_resource_id_fk", + "tableFrom": "resource_metadata", + "tableTo": "resource", + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_relationship": { + "name": "resource_relationship", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "from_identifier": { + "name": "from_identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_identifier": { + "name": "to_identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "resource_relationship_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "resource_relationship_to_identifier_from_identifier_index": { + "name": "resource_relationship_to_identifier_from_identifier_index", + "columns": [ + { + "expression": "to_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "from_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_relationship_workspace_id_workspace_id_fk": { + "name": "resource_relationship_workspace_id_workspace_id_fk", + "tableFrom": "resource_relationship", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_schema": { + "name": "resource_schema", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "json_schema": { + "name": "json_schema", + "type": "json", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "resource_schema_version_kind_workspace_id_index": { + "name": "resource_schema_version_kind_workspace_id_index", + "columns": [ + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_schema_workspace_id_workspace_id_fk": { + "name": "resource_schema_workspace_id_workspace_id_fk", + "tableFrom": "resource_schema", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_variable": { + "name": "resource_variable", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "reference": { + "name": "reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "default_value": { + "name": "default_value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "value_type": { + "name": "value_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'direct'" + } + }, + "indexes": { + "resource_variable_resource_id_key_index": { + "name": "resource_variable_resource_id_key_index", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_variable_resource_id_resource_id_fk": { + "name": "resource_variable_resource_id_resource_id_fk", + "tableFrom": "resource_variable", + "tableTo": "resource", + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_view": { + "name": "resource_view", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "filter": { + "name": "filter", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "resource_view_workspace_id_workspace_id_fk": { + "name": "resource_view_workspace_id_workspace_id_fk", + "tableFrom": "resource_view", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.azure_tenant": { + "name": "azure_tenant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "azure_tenant_tenant_id_index": { + "name": "azure_tenant_tenant_id_index", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "azure_tenant_workspace_id_workspace_id_fk": { + "name": "azure_tenant_workspace_id_workspace_id_fk", + "tableFrom": "azure_tenant", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_provider": { + "name": "resource_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "resource_provider_workspace_id_name_index": { + "name": "resource_provider_workspace_id_name_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_provider_workspace_id_workspace_id_fk": { + "name": "resource_provider_workspace_id_workspace_id_fk", + "tableFrom": "resource_provider", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_provider_aws": { + "name": "resource_provider_aws", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "resource_provider_id": { + "name": "resource_provider_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "aws_role_arns": { + "name": "aws_role_arns", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "import_eks": { + "name": "import_eks", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "import_vpc": { + "name": "import_vpc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "resource_provider_aws_resource_provider_id_resource_provider_id_fk": { + "name": "resource_provider_aws_resource_provider_id_resource_provider_id_fk", + "tableFrom": "resource_provider_aws", + "tableTo": "resource_provider", + "columnsFrom": ["resource_provider_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_provider_azure": { + "name": "resource_provider_azure", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "resource_provider_id": { + "name": "resource_provider_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "resource_provider_azure_resource_provider_id_resource_provider_id_fk": { + "name": "resource_provider_azure_resource_provider_id_resource_provider_id_fk", + "tableFrom": "resource_provider_azure", + "tableTo": "resource_provider", + "columnsFrom": ["resource_provider_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resource_provider_azure_tenant_id_azure_tenant_id_fk": { + "name": "resource_provider_azure_tenant_id_azure_tenant_id_fk", + "tableFrom": "resource_provider_azure", + "tableTo": "azure_tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_provider_google": { + "name": "resource_provider_google", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "resource_provider_id": { + "name": "resource_provider_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_ids": { + "name": "project_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "import_gke": { + "name": "import_gke", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "import_namespaces": { + "name": "import_namespaces", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "import_vcluster": { + "name": "import_vcluster", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "import_vms": { + "name": "import_vms", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "import_vpc": { + "name": "import_vpc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "resource_provider_google_resource_provider_id_resource_provider_id_fk": { + "name": "resource_provider_google_resource_provider_id_resource_provider_id_fk", + "tableFrom": "resource_provider_google", + "tableTo": "resource_provider", + "columnsFrom": ["resource_provider_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system": { + "name": "system", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "system_workspace_id_slug_index": { + "name": "system_workspace_id_slug_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "system_workspace_id_workspace_id_fk": { + "name": "system_workspace_id_workspace_id_fk", + "tableFrom": "system", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.runbook": { + "name": "runbook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_id": { + "name": "system_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_agent_id": { + "name": "job_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "job_agent_config": { + "name": "job_agent_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": { + "name_system_id_unique": { + "name": "name_system_id_unique", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "system_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "runbook_system_id_system_id_fk": { + "name": "runbook_system_id_system_id_fk", + "tableFrom": "runbook", + "tableTo": "system", + "columnsFrom": ["system_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "runbook_job_agent_id_job_agent_id_fk": { + "name": "runbook_job_agent_id_job_agent_id_fk", + "tableFrom": "runbook", + "tableTo": "job_agent", + "columnsFrom": ["job_agent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.runbook_job_trigger": { + "name": "runbook_job_trigger", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "runbook_id": { + "name": "runbook_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "runbook_job_trigger_job_id_job_id_fk": { + "name": "runbook_job_trigger_job_id_job_id_fk", + "tableFrom": "runbook_job_trigger", + "tableTo": "job", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "runbook_job_trigger_runbook_id_runbook_id_fk": { + "name": "runbook_job_trigger_runbook_id_runbook_id_fk", + "tableFrom": "runbook_job_trigger", + "tableTo": "runbook", + "columnsFrom": ["runbook_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "runbook_job_trigger_job_id_unique": { + "name": "runbook_job_trigger_job_id_unique", + "nullsNotDistinct": false, + "columns": ["job_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team": { + "name": "team", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "team_workspace_id_workspace_id_fk": { + "name": "team_workspace_id_workspace_id_fk", + "tableFrom": "team", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_member": { + "name": "team_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "team_member_team_id_user_id_index": { + "name": "team_member_team_id_user_id_index", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_member_team_id_team_id_fk": { + "name": "team_member_team_id_team_id_fk", + "tableFrom": "team_member", + "tableTo": "team", + "columnsFrom": ["team_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_member_user_id_user_id_fk": { + "name": "team_member_user_id_user_id_fk", + "tableFrom": "team_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job": { + "name": "job", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_agent_id": { + "name": "job_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "job_agent_config": { + "name": "job_agent_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "job_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'policy_passing'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_created_at_idx": { + "name": "job_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_status_idx": { + "name": "job_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_job_agent_id_job_agent_id_fk": { + "name": "job_job_agent_id_job_agent_id_fk", + "tableFrom": "job", + "tableTo": "job_agent", + "columnsFrom": ["job_agent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_metadata": { + "name": "job_metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "job_metadata_key_job_id_index": { + "name": "job_metadata_key_job_id_index", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_metadata_job_id_job_id_fk": { + "name": "job_metadata_job_id_job_id_fk", + "tableFrom": "job_metadata", + "tableTo": "job", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_variable": { + "name": "job_variable", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "job_variable_job_id_key_index": { + "name": "job_variable_job_id_key_index", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_variable_job_id_job_id_fk": { + "name": "job_variable_job_id_job_id_fk", + "tableFrom": "job_variable", + "tableTo": "job", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "google_service_account_email": { + "name": "google_service_account_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "aws_role_arn": { + "name": "aws_role_arn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_slug_unique": { + "name": "workspace_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_email_domain_matching": { + "name": "workspace_email_domain_matching", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "verification_code": { + "name": "verification_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification_email": { + "name": "verification_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_email_domain_matching_workspace_id_domain_index": { + "name": "workspace_email_domain_matching_workspace_id_domain_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_email_domain_matching_workspace_id_workspace_id_fk": { + "name": "workspace_email_domain_matching_workspace_id_workspace_id_fk", + "tableFrom": "workspace_email_domain_matching", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_email_domain_matching_role_id_role_id_fk": { + "name": "workspace_email_domain_matching_role_id_role_id_fk", + "tableFrom": "workspace_email_domain_matching", + "tableTo": "role", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.variable_set": { + "name": "variable_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_id": { + "name": "system_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "variable_set_system_id_system_id_fk": { + "name": "variable_set_system_id_system_id_fk", + "tableFrom": "variable_set", + "tableTo": "system", + "columnsFrom": ["system_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.variable_set_environment": { + "name": "variable_set_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "variable_set_id": { + "name": "variable_set_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "variable_set_environment_variable_set_id_variable_set_id_fk": { + "name": "variable_set_environment_variable_set_id_variable_set_id_fk", + "tableFrom": "variable_set_environment", + "tableTo": "variable_set", + "columnsFrom": ["variable_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "variable_set_environment_environment_id_environment_id_fk": { + "name": "variable_set_environment_environment_id_environment_id_fk", + "tableFrom": "variable_set_environment", + "tableTo": "environment", + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.variable_set_value": { + "name": "variable_set_value", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "variable_set_id": { + "name": "variable_set_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "variable_set_value_variable_set_id_key_index": { + "name": "variable_set_value_variable_set_id_key_index", + "columns": [ + { + "expression": "variable_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "variable_set_value_variable_set_id_variable_set_id_fk": { + "name": "variable_set_value_variable_set_id_variable_set_id_fk", + "tableFrom": "variable_set_value", + "tableTo": "variable_set", + "columnsFrom": ["variable_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_invite_token": { + "name": "workspace_invite_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_invite_token_role_id_role_id_fk": { + "name": "workspace_invite_token_role_id_role_id_fk", + "tableFrom": "workspace_invite_token", + "tableTo": "role", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invite_token_workspace_id_workspace_id_fk": { + "name": "workspace_invite_token_workspace_id_workspace_id_fk", + "tableFrom": "workspace_invite_token", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invite_token_created_by_user_id_fk": { + "name": "workspace_invite_token_created_by_user_id_fk", + "tableFrom": "workspace_invite_token", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_invite_token_token_unique": { + "name": "workspace_invite_token_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_metadata_group": { + "name": "resource_metadata_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keys": { + "name": "keys", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "include_null_combinations": { + "name": "include_null_combinations", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "resource_metadata_group_workspace_id_workspace_id_fk": { + "name": "resource_metadata_group_workspace_id_workspace_id_fk", + "tableFrom": "resource_metadata_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.runbook_variable": { + "name": "runbook_variable", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "runbook_id": { + "name": "runbook_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "required": { + "name": "required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "runbook_variable_runbook_id_key_index": { + "name": "runbook_variable_runbook_id_key_index", + "columns": [ + { + "expression": "runbook_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "runbook_variable_runbook_id_runbook_id_fk": { + "name": "runbook_variable_runbook_id_runbook_id_fk", + "tableFrom": "runbook_variable", + "tableTo": "runbook", + "columnsFrom": ["runbook_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.entity_role": { + "name": "entity_role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "scope_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "entity_role_role_id_entity_type_entity_id_scope_id_scope_type_index": { + "name": "entity_role_role_id_entity_type_entity_id_scope_id_scope_type_index", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "entity_role_role_id_role_id_fk": { + "name": "entity_role_role_id_role_id_fk", + "tableFrom": "entity_role", + "tableTo": "role", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role": { + "name": "role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "role_workspace_id_workspace_id_fk": { + "name": "role_workspace_id_workspace_id_fk", + "tableFrom": "role", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role_permission": { + "name": "role_permission", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "role_permission_role_id_permission_index": { + "name": "role_permission_role_id_permission_index", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "role_permission_role_id_role_id_fk": { + "name": "role_permission_role_id_role_id_fk", + "tableFrom": "role_permission", + "tableTo": "role", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_agent": { + "name": "job_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": { + "job_agent_workspace_id_name_index": { + "name": "job_agent_workspace_id_name_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_agent_workspace_id_workspace_id_fk": { + "name": "job_agent_workspace_id_workspace_id_fk", + "tableFrom": "job_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_policy_deployment_version_channel": { + "name": "environment_policy_deployment_version_channel", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "environment_policy_deployment_version_channel_policy_id_channel_id_index": { + "name": "environment_policy_deployment_version_channel_policy_id_channel_id_index", + "columns": [ + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_policy_deployment_version_channel_policy_id_deployment_id_index": { + "name": "environment_policy_deployment_version_channel_policy_id_deployment_id_index", + "columns": [ + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environment_policy_deployment_version_channel_policy_id_environment_policy_id_fk": { + "name": "environment_policy_deployment_version_channel_policy_id_environment_policy_id_fk", + "tableFrom": "environment_policy_deployment_version_channel", + "tableTo": "environment_policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_policy_deployment_version_channel_channel_id_deployment_version_channel_id_fk": { + "name": "environment_policy_deployment_version_channel_channel_id_deployment_version_channel_id_fk", + "tableFrom": "environment_policy_deployment_version_channel", + "tableTo": "deployment_version_channel", + "columnsFrom": ["channel_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_policy_deployment_version_channel_deployment_id_deployment_id_fk": { + "name": "environment_policy_deployment_version_channel_deployment_id_deployment_id_fk", + "tableFrom": "environment_policy_deployment_version_channel", + "tableTo": "deployment", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.computed_policy_target_release_target": { + "name": "computed_policy_target_release_target", + "schema": "", + "columns": { + "policy_target_id": { + "name": "policy_target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "release_target_id": { + "name": "release_target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "computed_policy_target_release_target_policy_target_id_policy_target_id_fk": { + "name": "computed_policy_target_release_target_policy_target_id_policy_target_id_fk", + "tableFrom": "computed_policy_target_release_target", + "tableTo": "policy_target", + "columnsFrom": ["policy_target_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "computed_policy_target_release_target_release_target_id_release_target_id_fk": { + "name": "computed_policy_target_release_target_release_target_id_release_target_id_fk", + "tableFrom": "computed_policy_target_release_target", + "tableTo": "release_target", + "columnsFrom": ["release_target_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "computed_policy_target_release_target_policy_target_id_release_target_id_pk": { + "name": "computed_policy_target_release_target_policy_target_id_release_target_id_pk", + "columns": ["policy_target_id", "release_target_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy": { + "name": "policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_workspace_id_workspace_id_fk": { + "name": "policy_workspace_id_workspace_id_fk", + "tableFrom": "policy", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "policy_workspace_id_name_unique": { + "name": "policy_workspace_id_name_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id", "name"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_target": { + "name": "policy_target", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "deployment_selector": { + "name": "deployment_selector", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "NULL" + }, + "environment_selector": { + "name": "environment_selector", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "NULL" + }, + "resource_selector": { + "name": "resource_selector", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "NULL" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_target_policy_id_policy_id_fk": { + "name": "policy_target_policy_id_policy_id_fk", + "tableFrom": "policy_target", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.release": { + "name": "release", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "version_release_id": { + "name": "version_release_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "variable_release_id": { + "name": "variable_release_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "release_version_release_id_version_release_id_fk": { + "name": "release_version_release_id_version_release_id_fk", + "tableFrom": "release", + "tableTo": "version_release", + "columnsFrom": ["version_release_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_variable_release_id_variable_set_release_id_fk": { + "name": "release_variable_release_id_variable_set_release_id_fk", + "tableFrom": "release", + "tableTo": "variable_set_release", + "columnsFrom": ["variable_release_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.release_job": { + "name": "release_job", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "release_id": { + "name": "release_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "release_job_release_id_release_id_fk": { + "name": "release_job_release_id_release_id_fk", + "tableFrom": "release_job", + "tableTo": "release", + "columnsFrom": ["release_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_job_job_id_job_id_fk": { + "name": "release_job_job_id_job_id_fk", + "tableFrom": "release_job", + "tableTo": "job", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.release_target": { + "name": "release_target", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "desired_release_id": { + "name": "desired_release_id", + "type": "uuid", + "primaryKey": false, + "notNull": false, + "default": "NULL" + } + }, + "indexes": { + "release_target_resource_id_environment_id_deployment_id_index": { + "name": "release_target_resource_id_environment_id_deployment_id_index", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "release_target_resource_id_resource_id_fk": { + "name": "release_target_resource_id_resource_id_fk", + "tableFrom": "release_target", + "tableTo": "resource", + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_target_environment_id_environment_id_fk": { + "name": "release_target_environment_id_environment_id_fk", + "tableFrom": "release_target", + "tableTo": "environment", + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_target_deployment_id_deployment_id_fk": { + "name": "release_target_deployment_id_deployment_id_fk", + "tableFrom": "release_target", + "tableTo": "deployment", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_target_desired_release_id_release_id_fk": { + "name": "release_target_desired_release_id_release_id_fk", + "tableFrom": "release_target", + "tableTo": "release", + "columnsFrom": ["desired_release_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.variable_set_release": { + "name": "variable_set_release", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "release_target_id": { + "name": "release_target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "variable_set_release_release_target_id_release_target_id_fk": { + "name": "variable_set_release_release_target_id_release_target_id_fk", + "tableFrom": "variable_set_release", + "tableTo": "release_target", + "columnsFrom": ["release_target_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.variable_set_release_value": { + "name": "variable_set_release_value", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "variable_set_release_id": { + "name": "variable_set_release_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "variable_value_snapshot_id": { + "name": "variable_value_snapshot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "variable_set_release_value_variable_set_release_id_variable_value_snapshot_id_index": { + "name": "variable_set_release_value_variable_set_release_id_variable_value_snapshot_id_index", + "columns": [ + { + "expression": "variable_set_release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "variable_value_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "variable_set_release_value_variable_set_release_id_variable_set_release_id_fk": { + "name": "variable_set_release_value_variable_set_release_id_variable_set_release_id_fk", + "tableFrom": "variable_set_release_value", + "tableTo": "variable_set_release", + "columnsFrom": ["variable_set_release_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "variable_set_release_value_variable_value_snapshot_id_variable_value_snapshot_id_fk": { + "name": "variable_set_release_value_variable_value_snapshot_id_variable_value_snapshot_id_fk", + "tableFrom": "variable_set_release_value", + "tableTo": "variable_value_snapshot", + "columnsFrom": ["variable_value_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.variable_value_snapshot": { + "name": "variable_value_snapshot", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "variable_value_snapshot_workspace_id_key_value_index": { + "name": "variable_value_snapshot_workspace_id_key_value_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "value", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "variable_value_snapshot_workspace_id_key_index": { + "name": "variable_value_snapshot_workspace_id_key_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"variable_value_snapshot\".\"value\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "variable_value_snapshot_workspace_id_workspace_id_fk": { + "name": "variable_value_snapshot_workspace_id_workspace_id_fk", + "tableFrom": "variable_value_snapshot", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.version_release": { + "name": "version_release", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "release_target_id": { + "name": "release_target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version_id": { + "name": "version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "version_release_release_target_id_release_target_id_fk": { + "name": "version_release_release_target_id_release_target_id_fk", + "tableFrom": "version_release", + "tableTo": "release_target", + "columnsFrom": ["release_target_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "version_release_version_id_deployment_version_id_fk": { + "name": "version_release_version_id_deployment_version_id_fk", + "tableFrom": "version_release", + "tableTo": "deployment_version", + "columnsFrom": ["version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_deny_window": { + "name": "policy_rule_deny_window", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "rrule": { + "name": "rrule", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "dtend": { + "name": "dtend", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "NULL" + }, + "time_zone": { + "name": "time_zone", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_deny_window_policy_id_policy_id_fk": { + "name": "policy_rule_deny_window_policy_id_policy_id_fk", + "tableFrom": "policy_rule_deny_window", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_user_approval": { + "name": "policy_rule_user_approval", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_user_approval_policy_id_policy_id_fk": { + "name": "policy_rule_user_approval_policy_id_policy_id_fk", + "tableFrom": "policy_rule_user_approval", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "policy_rule_user_approval_user_id_user_id_fk": { + "name": "policy_rule_user_approval_user_id_user_id_fk", + "tableFrom": "policy_rule_user_approval", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_user_approval_record": { + "name": "policy_rule_user_approval_record", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "approval_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "NULL" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rule_id": { + "name": "rule_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_user_approval_record_user_id_user_id_fk": { + "name": "policy_rule_user_approval_record_user_id_user_id_fk", + "tableFrom": "policy_rule_user_approval_record", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "policy_rule_user_approval_record_rule_id_policy_rule_user_approval_id_fk": { + "name": "policy_rule_user_approval_record_rule_id_policy_rule_user_approval_id_fk", + "tableFrom": "policy_rule_user_approval_record", + "tableTo": "policy_rule_user_approval", + "columnsFrom": ["rule_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_role_approval": { + "name": "policy_rule_role_approval", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "required_approvals_count": { + "name": "required_approvals_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_role_approval_policy_id_policy_id_fk": { + "name": "policy_rule_role_approval_policy_id_policy_id_fk", + "tableFrom": "policy_rule_role_approval", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "policy_rule_role_approval_role_id_role_id_fk": { + "name": "policy_rule_role_approval_role_id_role_id_fk", + "tableFrom": "policy_rule_role_approval", + "tableTo": "role", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_role_approval_record": { + "name": "policy_rule_role_approval_record", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "approval_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "NULL" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rule_id": { + "name": "rule_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_role_approval_record_user_id_user_id_fk": { + "name": "policy_rule_role_approval_record_user_id_user_id_fk", + "tableFrom": "policy_rule_role_approval_record", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "policy_rule_role_approval_record_rule_id_policy_rule_role_approval_id_fk": { + "name": "policy_rule_role_approval_record_rule_id_policy_rule_role_approval_id_fk", + "tableFrom": "policy_rule_role_approval_record", + "tableTo": "policy_rule_role_approval", + "columnsFrom": ["rule_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_any_approval": { + "name": "policy_rule_any_approval", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "required_approvals_count": { + "name": "required_approvals_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": { + "unique_policy_id": { + "name": "unique_policy_id", + "columns": [ + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "policy_rule_any_approval_policy_id_policy_id_fk": { + "name": "policy_rule_any_approval_policy_id_policy_id_fk", + "tableFrom": "policy_rule_any_approval", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_any_approval_record": { + "name": "policy_rule_any_approval_record", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "approval_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "NULL" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "unique_rule_id_user_id": { + "name": "unique_rule_id_user_id", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "policy_rule_any_approval_record_user_id_user_id_fk": { + "name": "policy_rule_any_approval_record_user_id_user_id_fk", + "tableFrom": "policy_rule_any_approval_record", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_deployment_version_selector": { + "name": "policy_rule_deployment_version_selector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_selector": { + "name": "deployment_version_selector", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_deployment_version_selector_policy_id_policy_id_fk": { + "name": "policy_rule_deployment_version_selector_policy_id_policy_id_fk", + "tableFrom": "policy_rule_deployment_version_selector", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "policy_rule_deployment_version_selector_policy_id_unique": { + "name": "policy_rule_deployment_version_selector_policy_id_unique", + "nullsNotDistinct": false, + "columns": ["policy_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_concurrency": { + "name": "policy_rule_concurrency", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "concurrency": { + "name": "concurrency", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_concurrency_policy_id_policy_id_fk": { + "name": "policy_rule_concurrency_policy_id_policy_id_fk", + "tableFrom": "policy_rule_concurrency", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "policy_rule_concurrency_policy_id_unique": { + "name": "policy_rule_concurrency_policy_id_unique", + "nullsNotDistinct": false, + "columns": ["policy_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_environment_version_rollout": { + "name": "policy_rule_environment_version_rollout", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "position_growth_factor": { + "name": "position_growth_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "time_scale_interval": { + "name": "time_scale_interval", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "rollout_type": { + "name": "rollout_type", + "type": "rollout_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'linear'" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_environment_version_rollout_policy_id_policy_id_fk": { + "name": "policy_rule_environment_version_rollout_policy_id_policy_id_fk", + "tableFrom": "policy_rule_environment_version_rollout", + "tableTo": "policy", + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "policy_rule_environment_version_rollout_policy_id_unique": { + "name": "policy_rule_environment_version_rollout_policy_id_unique", + "nullsNotDistinct": false, + "columns": ["policy_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_relationship_rule": { + "name": "resource_relationship_rule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference": { + "name": "reference", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dependency_type": { + "name": "dependency_type", + "type": "resource_dependency_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "dependency_description": { + "name": "dependency_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_version": { + "name": "source_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_version": { + "name": "target_version", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "unique_resource_relationship_rule_reference": { + "name": "unique_resource_relationship_rule_reference", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reference", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_relationship_rule_workspace_id_workspace_id_fk": { + "name": "resource_relationship_rule_workspace_id_workspace_id_fk", + "tableFrom": "resource_relationship_rule", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_relationship_rule_metadata_match": { + "name": "resource_relationship_rule_metadata_match", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "resource_relationship_rule_id": { + "name": "resource_relationship_rule_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "unique_resource_relationship_rule_metadata_match": { + "name": "unique_resource_relationship_rule_metadata_match", + "columns": [ + { + "expression": "resource_relationship_rule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_relationship_rule_metadata_match_resource_relationship_rule_id_resource_relationship_rule_id_fk": { + "name": "resource_relationship_rule_metadata_match_resource_relationship_rule_id_resource_relationship_rule_id_fk", + "tableFrom": "resource_relationship_rule_metadata_match", + "tableTo": "resource_relationship_rule", + "columnsFrom": ["resource_relationship_rule_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_relationship_rule_target_metadata_equals": { + "name": "resource_relationship_rule_target_metadata_equals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "resource_relationship_rule_id": { + "name": "resource_relationship_rule_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "unique_resource_relationship_rule_target_metadata_equals": { + "name": "unique_resource_relationship_rule_target_metadata_equals", + "columns": [ + { + "expression": "resource_relationship_rule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_relationship_rule_target_metadata_equals_resource_relationship_rule_id_resource_relationship_rule_id_fk": { + "name": "resource_relationship_rule_target_metadata_equals_resource_relationship_rule_id_resource_relationship_rule_id_fk", + "tableFrom": "resource_relationship_rule_target_metadata_equals", + "tableTo": "resource_relationship_rule", + "columnsFrom": ["resource_relationship_rule_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.system_role": { + "name": "system_role", + "schema": "public", + "values": ["user", "admin"] + }, + "public.deployment_version_status": { + "name": "deployment_version_status", + "schema": "public", + "values": ["building", "ready", "failed"] + }, + "public.environment_policy_approval_requirement": { + "name": "environment_policy_approval_requirement", + "schema": "public", + "values": ["manual", "automatic"] + }, + "public.approval_status_type": { + "name": "approval_status_type", + "schema": "public", + "values": ["pending", "approved", "rejected"] + }, + "public.environment_policy_deployment_success_type": { + "name": "environment_policy_deployment_success_type", + "schema": "public", + "values": ["all", "some", "optional"] + }, + "public.recurrence_type": { + "name": "recurrence_type", + "schema": "public", + "values": ["hourly", "daily", "weekly", "monthly"] + }, + "public.release_sequencing_type": { + "name": "release_sequencing_type", + "schema": "public", + "values": ["wait", "cancel"] + }, + "public.github_entity_type": { + "name": "github_entity_type", + "schema": "public", + "values": ["organization", "user"] + }, + "public.resource_relationship_type": { + "name": "resource_relationship_type", + "schema": "public", + "values": ["associated_with", "depends_on"] + }, + "public.job_reason": { + "name": "job_reason", + "schema": "public", + "values": [ + "policy_passing", + "policy_override", + "env_policy_override", + "config_policy_override" + ] + }, + "public.job_status": { + "name": "job_status", + "schema": "public", + "values": [ + "cancelled", + "skipped", + "in_progress", + "action_required", + "pending", + "failure", + "invalid_job_agent", + "invalid_integration", + "external_run_not_found", + "successful" + ] + }, + "public.entity_type": { + "name": "entity_type", + "schema": "public", + "values": ["user", "team"] + }, + "public.scope_type": { + "name": "scope_type", + "schema": "public", + "values": [ + "deploymentVersion", + "deploymentVersionChannel", + "resource", + "resourceProvider", + "resourceMetadataGroup", + "resourceRelationshipRule", + "workspace", + "environment", + "environmentPolicy", + "deploymentVariable", + "deploymentVariableValue", + "variableSet", + "system", + "deployment", + "job", + "jobAgent", + "runbook", + "policy", + "resourceView", + "releaseTarget" + ] + }, + "public.approval_status": { + "name": "approval_status", + "schema": "public", + "values": ["approved", "rejected"] + }, + "public.rollout_type": { + "name": "rollout_type", + "schema": "public", + "values": [ + "linear", + "linear_normalized", + "exponential", + "exponential_normalized" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 65e30de26..be23ac2bf 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -785,6 +785,13 @@ "when": 1748896977225, "tag": "0111_groovy_tombstone", "breakpoints": true + }, + { + "idx": 112, + "version": "7", + "when": 1749832657753, + "tag": "0112_steady_proteus", + "breakpoints": true } ] } diff --git a/packages/db/src/schema/policy-relations.ts b/packages/db/src/schema/policy-relations.ts index 9f94b074d..3c8e61e18 100644 --- a/packages/db/src/schema/policy-relations.ts +++ b/packages/db/src/schema/policy-relations.ts @@ -11,6 +11,7 @@ import { policyRuleAnyApproval, policyRuleDenyWindow, policyRuleDeploymentVersionSelector, + policyRuleEnvironmentVersionRollout, policyRuleRoleApproval, policyRuleUserApproval, } from "./rules/index.js"; @@ -30,6 +31,8 @@ export const policyRelations = relations(policy, ({ many, one }) => ({ versionAnyApprovals: one(policyRuleAnyApproval), concurrency: one(policyRuleConcurrency), + + environmentVersionRollout: one(policyRuleEnvironmentVersionRollout), })); export const policyTargetRelations = relations( diff --git a/packages/db/src/schema/policy.ts b/packages/db/src/schema/policy.ts index fa6b2c9b7..68b4da039 100644 --- a/packages/db/src/schema/policy.ts +++ b/packages/db/src/schema/policy.ts @@ -37,6 +37,7 @@ import { createPolicyRuleRoleApproval } from "./rules/approval-role.js"; import { createPolicyRuleUserApproval } from "./rules/approval-user.js"; import { createPolicyRuleDenyWindow } from "./rules/deny-window.js"; import { createPolicyRuleDeploymentVersionSelector } from "./rules/deployment-selector.js"; +import { createPolicyRuleEnvironmentVersionRollout } from "./rules/environment-version-rollout.js"; import { workspace } from "./workspace.js"; export const policy = pgTable( @@ -156,6 +157,11 @@ export const createPolicy = z.intersection( message: "Concurrency must be greater than 0", path: ["concurrency"], }), + + environmentVersionRollout: createPolicyRuleEnvironmentVersionRollout + .omit({ policyId: true }) + .optional() + .nullable(), }), ); export type CreatePolicy = z.infer; @@ -190,6 +196,11 @@ export const updatePolicy = policyInsertSchema.partial().extend({ message: "Concurrency must be greater than 0", path: ["concurrency"], }), + + environmentVersionRollout: createPolicyRuleEnvironmentVersionRollout + .omit({ policyId: true }) + .optional() + .nullable(), }); export type UpdatePolicy = z.infer; diff --git a/packages/db/src/schema/rules/environment-version-rollout.ts b/packages/db/src/schema/rules/environment-version-rollout.ts new file mode 100644 index 000000000..22a821e1d --- /dev/null +++ b/packages/db/src/schema/rules/environment-version-rollout.ts @@ -0,0 +1,87 @@ +import { pgEnum, pgTable, real, uuid } from "drizzle-orm/pg-core"; +import { createInsertSchema } from "drizzle-zod"; +import { z } from "zod"; + +import { policy } from "../policy.js"; +import { basePolicyRuleFields } from "./base.js"; + +export const rolloutType = pgEnum("rollout_type", [ + "linear", + "linear_normalized", + "exponential", + "exponential_normalized", +]); +export enum RolloutType { + Linear = "linear", + LinearNormalized = "linear_normalized", + Exponential = "exponential", + ExponentialNormalized = "exponential_normalized", +} +const ROLLOUT_TYPE_MAPPINGS = { + linear: { + api: "linear", + db: RolloutType.Linear, + }, + "linear-normalized": { + api: "linear-normalized", + db: RolloutType.LinearNormalized, + }, + exponential: { + api: "exponential", + db: RolloutType.Exponential, + }, + "exponential-normalized": { + api: "exponential-normalized", + db: RolloutType.ExponentialNormalized, + }, +} as const; + +export const apiRolloutTypeToDBRolloutType: Record = + Object.fromEntries( + Object.values(ROLLOUT_TYPE_MAPPINGS).map(({ api, db }) => [api, db]), + ); + +export const dbRolloutTypeToAPIRolloutType = Object.fromEntries( + Object.values(ROLLOUT_TYPE_MAPPINGS).map(({ api, db }) => [db, api]), +) as Record< + RolloutType, + "linear" | "exponential" | "linear-normalized" | "exponential-normalized" +>; + +export const policyRuleEnvironmentVersionRollout = pgTable( + "policy_rule_environment_version_rollout", + { + ...basePolicyRuleFields, + + policyId: uuid("policy_id") + .notNull() + .unique() + .references(() => policy.id, { onDelete: "cascade" }), + + positionGrowthFactor: real("position_growth_factor").notNull().default(1), + + timeScaleInterval: real("time_scale_interval").notNull(), + + rolloutType: rolloutType("rollout_type") + .notNull() + .default(RolloutType.Linear), + }, +); + +export type PolicyRuleEnvironmentVersionRollout = + typeof policyRuleEnvironmentVersionRollout.$inferSelect; + +export const createPolicyRuleEnvironmentVersionRollout = createInsertSchema( + policyRuleEnvironmentVersionRollout, + { + policyId: z.string().uuid(), + positionGrowthFactor: z.number().refine((val) => val > 0 && val <= 100), + timeScaleInterval: z.number().refine((val) => val > 0 && val <= 100), + rolloutType: z.enum([ + "linear", + "exponential", + "linear-normalized", + "exponential-normalized", + ]), + }, +).omit({ id: true }); diff --git a/packages/db/src/schema/rules/index.ts b/packages/db/src/schema/rules/index.ts index 58ab5fe5f..ed3160e26 100644 --- a/packages/db/src/schema/rules/index.ts +++ b/packages/db/src/schema/rules/index.ts @@ -10,3 +10,4 @@ export * from "./approval-any.js"; export * from "./rule-relations.js"; export * from "./deployment-selector.js"; export * from "./concurrency.js"; +export * from "./environment-version-rollout.js"; diff --git a/packages/db/src/schema/rules/rule-relations.ts b/packages/db/src/schema/rules/rule-relations.ts index 2d5717c65..019a344e8 100644 --- a/packages/db/src/schema/rules/rule-relations.ts +++ b/packages/db/src/schema/rules/rule-relations.ts @@ -18,6 +18,7 @@ import { import { policyRuleConcurrency } from "./concurrency.js"; import { policyRuleDenyWindow } from "./deny-window.js"; import { policyRuleDeploymentVersionSelector } from "./deployment-selector.js"; +import { policyRuleEnvironmentVersionRollout } from "./environment-version-rollout.js"; // User relations to approval records export const userApprovalRelations = relations(user, ({ many }) => ({ @@ -138,3 +139,13 @@ export const policyRuleConcurrencyRelations = relations( }), }), ); + +export const policyRuleEnvironmentVersionRolloutRelations = relations( + policyRuleEnvironmentVersionRollout, + ({ one }) => ({ + policy: one(policy, { + fields: [policyRuleEnvironmentVersionRollout.policyId], + references: [policy.id], + }), + }), +); diff --git a/packages/rule-engine/src/db/create-policy.ts b/packages/rule-engine/src/db/create-policy.ts index af0c983c7..e59e41e5d 100644 --- a/packages/rule-engine/src/db/create-policy.ts +++ b/packages/rule-engine/src/db/create-policy.ts @@ -93,6 +93,7 @@ export const createPolicyInTx = async (tx: Tx, input: CreatePolicyInput) => { versionUserApprovals, versionRoleApprovals, concurrency, + environmentVersionRollout, ...rest } = input; @@ -189,6 +190,27 @@ export const createPolicyInTx = async (tx: Tx, input: CreatePolicyInput) => { ]), }); + if (environmentVersionRollout != null) + await tx + .insert(SCHEMA.policyRuleEnvironmentVersionRollout) + .values({ + policyId, + ...environmentVersionRollout, + rolloutType: + environmentVersionRollout.rolloutType != null + ? SCHEMA.apiRolloutTypeToDBRolloutType[ + environmentVersionRollout.rolloutType + ] + : undefined, + }) + .onConflictDoUpdate({ + target: [SCHEMA.policyRuleEnvironmentVersionRollout.policyId], + set: buildConflictUpdateColumns( + SCHEMA.policyRuleEnvironmentVersionRollout, + ["positionGrowthFactor", "timeScaleInterval", "rolloutType"], + ), + }); + return { ...policy, targets, @@ -198,5 +220,6 @@ export const createPolicyInTx = async (tx: Tx, input: CreatePolicyInput) => { versionUserApprovals, versionRoleApprovals, concurrency, + environmentVersionRollout, }; }; diff --git a/packages/rule-engine/src/db/get-applicable-policies.ts b/packages/rule-engine/src/db/get-applicable-policies.ts index c532f5eb2..f28f31f8c 100644 --- a/packages/rule-engine/src/db/get-applicable-policies.ts +++ b/packages/rule-engine/src/db/get-applicable-policies.ts @@ -1,6 +1,6 @@ import type { Tx } from "@ctrlplane/db"; -import { eq } from "@ctrlplane/db"; +import { and, eq, inArray, isNull } from "@ctrlplane/db"; import * as schema from "@ctrlplane/db/schema"; import { withSpan } from "../span.js"; @@ -41,6 +41,7 @@ export const getApplicablePolicies = withSpan( versionRoleApprovals: true, versionUserApprovals: true, concurrency: true, + environmentVersionRollout: true, }, }, }, @@ -51,3 +52,54 @@ export const getApplicablePolicies = withSpan( return crts.map((crt) => crt.policyTarget.policy); }, ); + +export const getApplicablePoliciesWithoutResourceScope = async ( + db: Tx, + environmentId: string, + deploymentId: string, +) => { + const policyIdResults = await db + .selectDistinct({ policyId: schema.policy.id }) + .from(schema.computedPolicyTargetReleaseTarget) + .innerJoin( + schema.policyTarget, + eq( + schema.computedPolicyTargetReleaseTarget.policyTargetId, + schema.policyTarget.id, + ), + ) + .innerJoin( + schema.policy, + eq(schema.policyTarget.policyId, schema.policy.id), + ) + .innerJoin( + schema.releaseTarget, + eq( + schema.computedPolicyTargetReleaseTarget.releaseTargetId, + schema.releaseTarget.id, + ), + ) + .where( + and( + isNull(schema.policyTarget.resourceSelector), + eq(schema.releaseTarget.environmentId, environmentId), + eq(schema.releaseTarget.deploymentId, deploymentId), + eq(schema.policy.enabled, true), + ), + ); + + const policyIds = policyIdResults.map((r) => r.policyId); + if (policyIds.length === 0) return []; + return db.query.policy.findMany({ + where: inArray(schema.policy.id, policyIds), + with: { + denyWindows: true, + deploymentVersionSelector: true, + versionAnyApprovals: true, + versionRoleApprovals: true, + versionUserApprovals: true, + concurrency: true, + environmentVersionRollout: true, + }, + }); +}; diff --git a/packages/rule-engine/src/db/update-policy.ts b/packages/rule-engine/src/db/update-policy.ts index 14cade6ee..949017751 100644 --- a/packages/rule-engine/src/db/update-policy.ts +++ b/packages/rule-engine/src/db/update-policy.ts @@ -160,6 +160,40 @@ const updateConcurrency = async ( }); }; +const updateEnvironmentVersionRollout = async ( + tx: Tx, + policyId: string, + environmentVersionRollout: SCHEMA.UpdatePolicy["environmentVersionRollout"], +) => { + if (environmentVersionRollout === undefined) return; + if (environmentVersionRollout === null) + return tx + .delete(SCHEMA.policyRuleEnvironmentVersionRollout) + .where(eq(SCHEMA.policyRuleEnvironmentVersionRollout.policyId, policyId)); + + const { positionGrowthFactor, timeScaleInterval, rolloutType } = + environmentVersionRollout; + + await tx + .insert(SCHEMA.policyRuleEnvironmentVersionRollout) + .values({ + policyId, + positionGrowthFactor, + timeScaleInterval, + rolloutType: + rolloutType != null + ? SCHEMA.apiRolloutTypeToDBRolloutType[rolloutType] + : undefined, + }) + .onConflictDoUpdate({ + target: [SCHEMA.policyRuleEnvironmentVersionRollout.policyId], + set: buildConflictUpdateColumns( + SCHEMA.policyRuleEnvironmentVersionRollout, + ["positionGrowthFactor", "timeScaleInterval", "rolloutType"], + ), + }); +}; + export const updatePolicyInTx = async ( tx: Tx, id: string, @@ -173,6 +207,7 @@ export const updatePolicyInTx = async ( versionUserApprovals, versionRoleApprovals, concurrency, + environmentVersionRollout, ...rest } = input; @@ -198,6 +233,7 @@ export const updatePolicyInTx = async ( updateVersionUserApprovals(tx, policy.id, versionUserApprovals), updateVersionRoleApprovals(tx, policy.id, versionRoleApprovals), updateConcurrency(tx, policy.id, concurrency), + updateEnvironmentVersionRollout(tx, policy.id, environmentVersionRollout), ]); const updatedPolicy = await tx.query.policy.findFirst({ @@ -210,11 +246,26 @@ export const updatePolicyInTx = async ( versionUserApprovals: true, versionRoleApprovals: true, concurrency: true, + environmentVersionRollout: true, }, }); if (updatedPolicy == null) throw new Error("Policy not found"); const updatedConcurrency = updatedPolicy.concurrency?.concurrency; - return { ...updatedPolicy, concurrency: updatedConcurrency }; + const updatedEnvironmentVersionRollout = + updatedPolicy.environmentVersionRollout != null + ? { + ...updatedPolicy.environmentVersionRollout, + rolloutType: + SCHEMA.dbRolloutTypeToAPIRolloutType[ + updatedPolicy.environmentVersionRollout.rolloutType + ], + } + : null; + return { + ...updatedPolicy, + concurrency: updatedConcurrency, + environmentVersionRollout: updatedEnvironmentVersionRollout, + }; }; diff --git a/packages/rule-engine/src/index.ts b/packages/rule-engine/src/index.ts index 41fa83fe1..c252e123f 100644 --- a/packages/rule-engine/src/index.ts +++ b/packages/rule-engine/src/index.ts @@ -1,5 +1,5 @@ export * from "./manager/version-rule-engine.js"; -export * from "./manager/version-manager-rules.js"; +export * from "./manager/version-manager-rules/index.js"; export * from "./rules/index.js"; export * from "./utils/merge-policies.js"; export * from "./types.js"; diff --git a/packages/rule-engine/src/manager/version-manager-rules.ts b/packages/rule-engine/src/manager/version-manager-rules.ts index 7f35915a8..a3f38f5fe 100644 --- a/packages/rule-engine/src/manager/version-manager-rules.ts +++ b/packages/rule-engine/src/manager/version-manager-rules.ts @@ -8,11 +8,9 @@ import { ConcurrencyRule } from "../rules/concurrency-rule.js"; import { DeploymentDenyRule } from "../rules/deployment-deny-rule.js"; import { ReleaseTargetConcurrencyRule } from "../rules/release-target-concurrency-rule.js"; import { - getAnyApprovalRecords, - getRoleApprovalRecords, - getUserApprovalRecords, - VersionApprovalRule, -} from "../rules/version-approval-rule.js"; + getEnvironmentVersionRolloutRule, + getVersionApprovalRules, +} from "./version-manager-rules/index.js"; export const denyWindows = (policy: Policy | null) => policy == null @@ -26,52 +24,6 @@ export const denyWindows = (policy: Policy | null) => }), ); -export const versionAnyApprovalRule = ( - approvalRules?: Policy["versionAnyApprovals"] | null, -) => { - if (approvalRules == null) return []; - return [ - new VersionApprovalRule({ - minApprovals: approvalRules.requiredApprovalsCount, - getApprovalRecords: getAnyApprovalRecords, - }), - ]; -}; - -export const versionRoleApprovalRule = ( - approvalRules?: Policy["versionRoleApprovals"] | null, -) => { - if (approvalRules == null) return []; - return approvalRules.map( - (approval) => - new VersionApprovalRule({ - minApprovals: approval.requiredApprovalsCount, - getApprovalRecords: getRoleApprovalRecords, - }), - ); -}; - -export const versionUserApprovalRule = ( - approvalRules?: Policy["versionUserApprovals"] | null, -) => { - if (approvalRules == null) return []; - return approvalRules.map( - () => - new VersionApprovalRule({ - minApprovals: 1, - getApprovalRecords: getUserApprovalRecords, - }), - ); -}; - -export const getVersionApprovalRules = ( - policy: Policy | null, -): FilterRule[] => [ - ...versionUserApprovalRule(policy?.versionUserApprovals), - ...versionAnyApprovalRule(policy?.versionAnyApprovals), - ...versionRoleApprovalRule(policy?.versionRoleApprovals), -]; - export const getConcurrencyRule = (policy: Policy | null) => { if (policy?.concurrency == null) return []; const getReleaseTargetsInConcurrencyGroup = () => @@ -103,14 +55,19 @@ export const getConcurrencyRule = (policy: Policy | null) => { ]; }; -export const getRules = ( +export const getRules = async ( policy: Policy | null, releaseTargetId: string, -): Array | PreValidationRule> => { +): Promise | PreValidationRule>> => { + const environmentVersionRolloutRule = await getEnvironmentVersionRolloutRule( + policy, + releaseTargetId, + ); return [ new ReleaseTargetConcurrencyRule(releaseTargetId), ...getConcurrencyRule(policy), ...getVersionApprovalRules(policy), + ...(environmentVersionRolloutRule ? [environmentVersionRolloutRule] : []), ]; // The rrule package is being stupid and deny windows is not top priority // right now so I am commenting this out diff --git a/packages/rule-engine/src/manager/version-manager-rules/environment-version-rollout.ts b/packages/rule-engine/src/manager/version-manager-rules/environment-version-rollout.ts new file mode 100644 index 000000000..3ed75ef2b --- /dev/null +++ b/packages/rule-engine/src/manager/version-manager-rules/environment-version-rollout.ts @@ -0,0 +1,193 @@ +import type { Tx } from "@ctrlplane/db"; +import { addMinutes, startOfMinute } from "date-fns"; +import _ from "lodash"; + +import { and, count, eq, sql, takeFirst } from "@ctrlplane/db"; +import { db } from "@ctrlplane/db/client"; +import * as schema from "@ctrlplane/db/schema"; + +import type { Policy } from "../../types.js"; +import type { Version } from "../version-rule-engine.js"; +import { + EnvironmentVersionRolloutRule, + RolloutTypeToOffsetFunction, +} from "../../rules/environment-version-rollout-rule.js"; +import { + getAnyApprovalRecords, + getRoleApprovalRecords, + getUserApprovalRecords, +} from "../../rules/version-approval-rule.js"; +import { getVersionApprovalRules } from "./version-approval.js"; + +const getRolloutStartTimeGetter = + (policy: Policy | null) => async (version: Version) => { + const versionApprovalRules = getVersionApprovalRules(policy); + if (versionApprovalRules.length === 0) return version.createdAt; + + // Check if version passes all approval rules + const allApprovalsPassed = await versionApprovalRules.reduce( + async (passedSoFar, rule) => { + if (!(await passedSoFar)) return false; + const { allowedCandidates } = await rule.filter([version]); + return allowedCandidates.length > 0; + }, + Promise.resolve(true), + ); + + if (!allApprovalsPassed) return null; + + // Get most recent approval timestamp + const allApprovalRecords = [ + ...(await getAnyApprovalRecords([version.id])), + ...(await getUserApprovalRecords([version.id])), + ...(await getRoleApprovalRecords([version.id])), + ]; + + // technically this should never happen due to the approval rules having passed to get to this point + // but we check because maxBy can return undefined if the array is empty + if (allApprovalRecords.length === 0) return version.createdAt; + const latestApprovalRecord = _.chain(allApprovalRecords) + .filter((record) => record.approvedAt != null) + .maxBy((record) => record.approvedAt) + .value(); + + return latestApprovalRecord.approvedAt ?? version.createdAt; + }; + +const getNumReleaseTargets = async (db: Tx, releaseTargetId: string) => { + const releaseTarget = await db + .select() + .from(schema.releaseTarget) + .where(eq(schema.releaseTarget.id, releaseTargetId)) + .then(takeFirst); + + return db + .select({ count: count() }) + .from(schema.releaseTarget) + .where( + and( + eq(schema.releaseTarget.deploymentId, releaseTarget.deploymentId), + eq(schema.releaseTarget.environmentId, releaseTarget.environmentId), + ), + ) + .then(takeFirst) + .then((r) => r.count); +}; + +const getReleaseTargetPositionGetter = + (releaseTargetId: string) => async (version: Version) => { + const releaseTarget = await db.query.releaseTarget.findFirst({ + where: eq(schema.releaseTarget.id, releaseTargetId), + }); + + if (releaseTarget == null) + throw new Error(`Release target ${releaseTargetId} not found`); + + const orderedTargetsSubquery = db + .select({ + id: schema.releaseTarget.id, + position: + sql`ROW_NUMBER() OVER (ORDER BY md5(${schema.releaseTarget.id}::text || ${version.id}::text) ASC) - 1`.as( + "position", + ), + }) + .from(schema.releaseTarget) + .where( + and( + eq(schema.releaseTarget.environmentId, releaseTarget.environmentId), + eq(schema.releaseTarget.deploymentId, releaseTarget.deploymentId), + ), + ) + .as("ordered_targets"); + + return db + .select() + .from(orderedTargetsSubquery) + .where(eq(orderedTargetsSubquery.id, releaseTargetId)) + .then(takeFirst) + .then((r) => r.position); + }; + +const getDeploymentOffsetEquation = ( + envVersionRollout: schema.PolicyRuleEnvironmentVersionRollout, + numReleaseTargets: number, +) => { + const { rolloutType, positionGrowthFactor, timeScaleInterval } = + envVersionRollout; + + return RolloutTypeToOffsetFunction[rolloutType]( + positionGrowthFactor, + timeScaleInterval, + numReleaseTargets, + ); +}; + +export const getEnvironmentVersionRolloutRule = async ( + policy: Policy | null, + releaseTargetId: string, +) => { + if (policy?.environmentVersionRollout == null) return null; + if (policy.environmentVersionRollout.positionGrowthFactor <= 0) + throw new Error( + "Position growth factor must be greater than 0 for environment version rollout", + ); + + const getRolloutStartTime = getRolloutStartTimeGetter(policy); + const getReleaseTargetPosition = + getReleaseTargetPositionGetter(releaseTargetId); + const numReleaseTargets = await getNumReleaseTargets(db, releaseTargetId); + const getDeploymentOffsetMinutes = getDeploymentOffsetEquation( + policy.environmentVersionRollout, + numReleaseTargets, + ); + + return new EnvironmentVersionRolloutRule({ + getRolloutStartTime, + getReleaseTargetPosition, + getDeploymentOffsetMinutes, + }); +}; + +type ReleaseTarget = schema.ReleaseTarget & { + deployment: schema.Deployment; + environment: schema.Environment; + resource: schema.Resource; +}; + +export const getRolloutInfoForReleaseTarget = async ( + db: Tx, + releaseTarget: ReleaseTarget, + policy: Policy | null, + deploymentVersion: schema.DeploymentVersion, +): Promise< + ReleaseTarget & { rolloutTime: Date | null; rolloutPosition: number } +> => { + const environmentVersionRollout = policy?.environmentVersionRollout; + if (environmentVersionRollout == null) + return { + ...releaseTarget, + rolloutTime: deploymentVersion.createdAt, + rolloutPosition: 0, + }; + + const rolloutStartTime = + await getRolloutStartTimeGetter(policy)(deploymentVersion); + const rolloutPosition = await getReleaseTargetPositionGetter( + releaseTarget.id, + )(deploymentVersion); + + if (rolloutStartTime == null) + return { ...releaseTarget, rolloutTime: null, rolloutPosition }; + + const numReleaseTargets = await getNumReleaseTargets(db, releaseTarget.id); + const deploymentOffsetMinutes = getDeploymentOffsetEquation( + environmentVersionRollout, + numReleaseTargets, + )(rolloutPosition); + const rolloutTime = addMinutes( + startOfMinute(rolloutStartTime), + deploymentOffsetMinutes, + ); + + return { ...releaseTarget, rolloutTime, rolloutPosition }; +}; diff --git a/packages/rule-engine/src/manager/version-manager-rules/index.ts b/packages/rule-engine/src/manager/version-manager-rules/index.ts new file mode 100644 index 000000000..33eced4e8 --- /dev/null +++ b/packages/rule-engine/src/manager/version-manager-rules/index.ts @@ -0,0 +1,2 @@ +export * from "./environment-version-rollout.js"; +export * from "./version-approval.js"; diff --git a/packages/rule-engine/src/manager/version-manager-rules/version-approval.ts b/packages/rule-engine/src/manager/version-manager-rules/version-approval.ts new file mode 100644 index 000000000..a7b91103f --- /dev/null +++ b/packages/rule-engine/src/manager/version-manager-rules/version-approval.ts @@ -0,0 +1,54 @@ +import type { FilterRule, Policy } from "../../types.js"; +import type { Version } from "../version-rule-engine.js"; +import { + getAnyApprovalRecords, + getRoleApprovalRecords, + getUserApprovalRecords, + VersionApprovalRule, +} from "../../rules/version-approval-rule.js"; + +export const versionAnyApprovalRule = ( + approvalRules?: Policy["versionAnyApprovals"] | null, +) => { + if (approvalRules == null) return []; + return [ + new VersionApprovalRule({ + minApprovals: approvalRules.requiredApprovalsCount, + getApprovalRecords: getAnyApprovalRecords, + }), + ]; +}; + +export const versionRoleApprovalRule = ( + approvalRules?: Policy["versionRoleApprovals"] | null, +) => { + if (approvalRules == null) return []; + return approvalRules.map( + (approval) => + new VersionApprovalRule({ + minApprovals: approval.requiredApprovalsCount, + getApprovalRecords: getRoleApprovalRecords, + }), + ); +}; + +export const versionUserApprovalRule = ( + approvalRules?: Policy["versionUserApprovals"] | null, +) => { + if (approvalRules == null) return []; + return approvalRules.map( + () => + new VersionApprovalRule({ + minApprovals: 1, + getApprovalRecords: getUserApprovalRecords, + }), + ); +}; + +export const getVersionApprovalRules = ( + policy: Policy | null, +): FilterRule[] => [ + ...versionUserApprovalRule(policy?.versionUserApprovals), + ...versionAnyApprovalRule(policy?.versionAnyApprovals), + ...versionRoleApprovalRule(policy?.versionRoleApprovals), +]; diff --git a/packages/rule-engine/src/manager/version-manager.ts b/packages/rule-engine/src/manager/version-manager.ts index f5ec0ef3f..0dedf22e4 100644 --- a/packages/rule-engine/src/manager/version-manager.ts +++ b/packages/rule-engine/src/manager/version-manager.ts @@ -180,7 +180,8 @@ export class VersionReleaseManager implements ReleaseManager { async evaluate(options?: VersionEvaluateOptions) { const policy = options?.policy ?? (await this.getPolicy()); - const rules = (options?.rules ?? getRules)(policy, this.releaseTarget.id); + const ruleGetter = options?.rules ?? getRules; + const rules = await ruleGetter(policy, this.releaseTarget.id); const engine = new VersionRuleEngine(rules); const versions = diff --git a/packages/rule-engine/src/manager/version-rule-engine.ts b/packages/rule-engine/src/manager/version-rule-engine.ts index ac13f8884..700e05d41 100644 --- a/packages/rule-engine/src/manager/version-rule-engine.ts +++ b/packages/rule-engine/src/manager/version-rule-engine.ts @@ -63,14 +63,13 @@ export class VersionRuleEngine implements RuleEngine { for (const rule of preValidationRules) { const result = await rule.passing(); - if (!result.passing) { + if (!result.passing) return { chosenCandidate: null, rejectionReasons: new ConstantMap( result.rejectionReason ?? "", ), }; - } } let rejectionReasons = new Map(); @@ -80,20 +79,18 @@ export class VersionRuleEngine implements RuleEngine { const result = await rule.filter(candidates); // If the rule yields no candidates, we must stop. - if (result.allowedCandidates.length === 0) { + if (result.allowedCandidates.length === 0) return { chosenCandidate: null, rejectionReasons: result.rejectionReasons ?? rejectionReasons, }; - } // Merge any new rejection reasons with our tracking map - if (result.rejectionReasons) { + if (result.rejectionReasons) rejectionReasons = new Map([ ...rejectionReasons, ...result.rejectionReasons, ]); - } candidates = result.allowedCandidates; } @@ -101,14 +98,8 @@ export class VersionRuleEngine implements RuleEngine { // Once all rules pass, select the final version const chosen = this.selectFinalRelease(candidates); return chosen == null - ? { - chosenCandidate: null, - rejectionReasons, - } - : { - chosenCandidate: chosen, - rejectionReasons, - }; + ? { chosenCandidate: null, rejectionReasons } + : { chosenCandidate: chosen, rejectionReasons }; } /** diff --git a/packages/rule-engine/src/rules/environment-version-rollout-rule.ts b/packages/rule-engine/src/rules/environment-version-rollout-rule.ts new file mode 100644 index 000000000..face16462 --- /dev/null +++ b/packages/rule-engine/src/rules/environment-version-rollout-rule.ts @@ -0,0 +1,115 @@ +import { addMinutes, isAfter, isEqual, startOfMinute } from "date-fns"; + +import * as schema from "@ctrlplane/db/schema"; + +import type { Version } from "../manager/version-rule-engine.js"; +import type { FilterRule, RuleEngineRuleResult } from "../types.js"; + +type GetDeploymentOffsetMinutes = (targetPosition: number) => number; + +type OffsetFunctionGetter = ( + positionGrowthFactor: number, + timeScaleInterval: number, + numReleaseTargets: number, +) => GetDeploymentOffsetMinutes; + +const linearDeploymentOffset: OffsetFunctionGetter = + ( + _: number, + timeScaleInterval: number, + __: number, + ): GetDeploymentOffsetMinutes => + (x: number) => + timeScaleInterval * x; + +const linearDeploymentOffsetNormalized: OffsetFunctionGetter = + ( + _: number, + timeScaleInterval: number, + numReleaseTargets: number, + ): GetDeploymentOffsetMinutes => + (x: number) => + timeScaleInterval * (x / numReleaseTargets); + +const exponentialDeploymentOffset: OffsetFunctionGetter = + ( + positionGrowthFactor: number, + timeScaleInterval: number, + _: number, + ): GetDeploymentOffsetMinutes => + (x: number) => + timeScaleInterval * (1 - Math.exp(-x / positionGrowthFactor)); + +const exponentialDeploymentOffsetNormalized: OffsetFunctionGetter = + ( + positionGrowthFactor: number, + timeScaleInterval: number, + numReleaseTargets: number, + ): GetDeploymentOffsetMinutes => + (x: number) => + timeScaleInterval * + ((1 - Math.exp(-x / numReleaseTargets)) / + (1 - Math.exp(-numReleaseTargets / positionGrowthFactor))); + +export const RolloutTypeToOffsetFunction: Record< + schema.RolloutType, + OffsetFunctionGetter +> = { + [schema.RolloutType.Linear]: linearDeploymentOffset, + [schema.RolloutType.Exponential]: exponentialDeploymentOffset, + [schema.RolloutType.LinearNormalized]: linearDeploymentOffsetNormalized, + [schema.RolloutType.ExponentialNormalized]: + exponentialDeploymentOffsetNormalized, +}; + +type EnvironmentVersionRolloutRuleOptions = { + getRolloutStartTime: (version: Version) => Promise; + getReleaseTargetPosition: (version: Version) => number | Promise; + getDeploymentOffsetMinutes: GetDeploymentOffsetMinutes; + skipReason?: string; +}; + +export class EnvironmentVersionRolloutRule implements FilterRule { + public readonly name = "EnvironmentVersionRolloutRule"; + + constructor(private readonly options: EnvironmentVersionRolloutRuleOptions) {} + + protected getCurrentTime() { + return new Date(); + } + + async getDeploymentTime(version: Version, startTime: Date) { + const targetPosition = await this.options.getReleaseTargetPosition(version); + const minutes = this.options.getDeploymentOffsetMinutes(targetPosition); + return addMinutes(startOfMinute(startTime), minutes); + } + + async filter(candidates: Version[]): Promise> { + const now = this.getCurrentTime(); + const rejectionReasons = new Map(); + const skip = + this.options.skipReason ?? + "Version not eligible for deployment in the current time window"; + + for (const candidate of candidates) { + const startTime = await this.options.getRolloutStartTime(candidate); + if (startTime == null) { + rejectionReasons.set( + candidate.id, + "Rollout has not started for this version", + ); + continue; + } + const deploymentTime = await this.getDeploymentTime(candidate, startTime); + + const isEligible = + isAfter(now, deploymentTime) || isEqual(now, deploymentTime); + if (!isEligible) rejectionReasons.set(candidate.id, skip); + } + + return { + allowedCandidates: candidates.filter((c) => !rejectionReasons.has(c.id)), + rejectionReasons, + }; + } +} diff --git a/packages/rule-engine/src/rules/version-approval-rule.ts b/packages/rule-engine/src/rules/version-approval-rule.ts index 50f36a128..7eb115861 100644 --- a/packages/rule-engine/src/rules/version-approval-rule.ts +++ b/packages/rule-engine/src/rules/version-approval-rule.ts @@ -12,6 +12,7 @@ type Record = { status: "approved" | "rejected"; userId: string; reason: string | null; + approvedAt: Date | null; }; export type GetApprovalRecordsFunc = ( diff --git a/packages/rule-engine/src/types.ts b/packages/rule-engine/src/types.ts index 467184ed6..1f7c9d0b2 100644 --- a/packages/rule-engine/src/types.ts +++ b/packages/rule-engine/src/types.ts @@ -70,6 +70,7 @@ export type Policy = schema.Policy & { versionUserApprovals: schema.PolicyRuleUserApproval[]; versionRoleApprovals: schema.PolicyRuleRoleApproval[]; concurrency: schema.PolicyRuleConcurrency | null; + environmentVersionRollout: schema.PolicyRuleEnvironmentVersionRollout | null; }; export type ReleaseTargetIdentifier = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13ded7830..1e901aedf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -810,6 +810,9 @@ importers: '@types/uuid': specifier: ^10.0.0 version: 10.0.0 + date-fns: + specifier: 'catalog:' + version: 3.6.0 eslint: specifier: 'catalog:' version: 9.11.1(jiti@2.3.3)