From 7e1308cf8edbe4eec7ac42cf6a5194127bcaf92c Mon Sep 17 00:00:00 2001 From: Michael Leone Date: Mon, 11 May 2026 11:58:07 -0400 Subject: [PATCH 01/13] feat(secrets): add secret_provider schema and Go AES-256-CBC crypto - packages/db: new `secret_provider` table (workspace-scoped, encrypted bytea config, unique by (workspace_id, name)) with `secret_provider_type` enum covering aws_secrets_manager, doppler, env. Drizzle migration 0195. - apps/workspace-engine/pkg/crypto: AES-256-CBC implementation matching the TypeScript @ctrlplane/secrets format (`:`, PKCS#7 padding, 32-byte hex key). Lets Go decrypt configs written by the TS api using the same VARIABLES_AES_256_KEY. - Interop golden tests: 4 ciphertexts produced by @ctrlplane/secrets are decrypted byte-for-byte by the Go implementation, plus round-trip and malformed-input coverage. --- apps/workspace-engine/pkg/crypto/aes256cbc.go | 127 + .../pkg/crypto/aes256cbc_test.go | 137 + .../db/drizzle/0195_unique_cobalt_man.sql | 13 + packages/db/drizzle/meta/0195_snapshot.json | 7607 +++++++++++++++++ packages/db/drizzle/meta/_journal.json | 9 +- packages/db/src/schema/index.ts | 1 + packages/db/src/schema/secret-provider.ts | 56 + 7 files changed, 7949 insertions(+), 1 deletion(-) create mode 100644 apps/workspace-engine/pkg/crypto/aes256cbc.go create mode 100644 apps/workspace-engine/pkg/crypto/aes256cbc_test.go create mode 100644 packages/db/drizzle/0195_unique_cobalt_man.sql create mode 100644 packages/db/drizzle/meta/0195_snapshot.json create mode 100644 packages/db/src/schema/secret-provider.ts diff --git a/apps/workspace-engine/pkg/crypto/aes256cbc.go b/apps/workspace-engine/pkg/crypto/aes256cbc.go new file mode 100644 index 000000000..94b573511 --- /dev/null +++ b/apps/workspace-engine/pkg/crypto/aes256cbc.go @@ -0,0 +1,127 @@ +// Package crypto provides AES-256-CBC encryption matching the format produced +// by the TypeScript @ctrlplane/secrets package. Both sides use a 32-byte key +// (encoded as 64 hex characters) and emit ciphertext in the form +// ":" with PKCS#7 padding. +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "strings" +) + +const ( + keyHexLength = 64 + keyByteLen = 32 + ivByteLen = 16 +) + +// AES256CBC encrypts and decrypts strings using AES-256-CBC. The key must be +// supplied as a 64-character hex string (32 bytes decoded). +type AES256CBC struct { + key []byte +} + +// New constructs an AES256CBC from a 64-character hex key. +func New(keyHex string) (*AES256CBC, error) { + if len(keyHex) != keyHexLength { + return nil, fmt.Errorf( + "aes256cbc: key must be %d hex characters, got %d", + keyHexLength, + len(keyHex), + ) + } + key, err := hex.DecodeString(keyHex) + if err != nil { + return nil, fmt.Errorf("aes256cbc: invalid hex key: %w", err) + } + if len(key) != keyByteLen { + return nil, fmt.Errorf( + "aes256cbc: decoded key must be %d bytes, got %d", + keyByteLen, + len(key), + ) + } + return &AES256CBC{key: key}, nil +} + +// Encrypt returns the ciphertext for plaintext in the format used by +// @ctrlplane/secrets: ":". +func (a *AES256CBC) Encrypt(plaintext string) (string, error) { + block, err := aes.NewCipher(a.key) + if err != nil { + return "", fmt.Errorf("aes256cbc: cipher init: %w", err) + } + iv := make([]byte, ivByteLen) + if _, err := rand.Read(iv); err != nil { + return "", fmt.Errorf("aes256cbc: iv generation: %w", err) + } + padded := pkcs7Pad([]byte(plaintext), block.BlockSize()) + encrypted := make([]byte, len(padded)) + cipher.NewCBCEncrypter(block, iv).CryptBlocks(encrypted, padded) + return hex.EncodeToString(iv) + ":" + hex.EncodeToString(encrypted), nil +} + +// Decrypt reverses Encrypt. It accepts ciphertexts produced by either the Go +// implementation or the TypeScript @ctrlplane/secrets package. +func (a *AES256CBC) Decrypt(ciphertext string) (string, error) { + ivHex, encHex, ok := strings.Cut(ciphertext, ":") + if !ok { + return "", errors.New("aes256cbc: invalid encrypted data") + } + iv, err := hex.DecodeString(ivHex) + if err != nil { + return "", fmt.Errorf("aes256cbc: invalid iv: %w", err) + } + if len(iv) != ivByteLen { + return "", fmt.Errorf("aes256cbc: iv must be %d bytes, got %d", ivByteLen, len(iv)) + } + enc, err := hex.DecodeString(encHex) + if err != nil { + return "", fmt.Errorf("aes256cbc: invalid ciphertext: %w", err) + } + block, err := aes.NewCipher(a.key) + if err != nil { + return "", fmt.Errorf("aes256cbc: cipher init: %w", err) + } + if len(enc) == 0 || len(enc)%block.BlockSize() != 0 { + return "", errors.New("aes256cbc: ciphertext length is not a multiple of block size") + } + decrypted := make([]byte, len(enc)) + cipher.NewCBCDecrypter(block, iv).CryptBlocks(decrypted, enc) + unpadded, err := pkcs7Unpad(decrypted, block.BlockSize()) + if err != nil { + return "", err + } + return string(unpadded), nil +} + +func pkcs7Pad(data []byte, blockSize int) []byte { + padLen := blockSize - len(data)%blockSize + padded := make([]byte, len(data)+padLen) + copy(padded, data) + for i := len(data); i < len(padded); i++ { + padded[i] = byte(padLen) + } + return padded +} + +func pkcs7Unpad(data []byte, blockSize int) ([]byte, error) { + if len(data) == 0 || len(data)%blockSize != 0 { + return nil, errors.New("aes256cbc: padded data is not a multiple of block size") + } + padLen := int(data[len(data)-1]) + if padLen == 0 || padLen > blockSize { + return nil, errors.New("aes256cbc: invalid padding length") + } + for i := len(data) - padLen; i < len(data); i++ { + if int(data[i]) != padLen { + return nil, errors.New("aes256cbc: invalid padding bytes") + } + } + return data[:len(data)-padLen], nil +} diff --git a/apps/workspace-engine/pkg/crypto/aes256cbc_test.go b/apps/workspace-engine/pkg/crypto/aes256cbc_test.go new file mode 100644 index 000000000..4c94b0ce0 --- /dev/null +++ b/apps/workspace-engine/pkg/crypto/aes256cbc_test.go @@ -0,0 +1,137 @@ +package crypto + +import ( + "strings" + "testing" +) + +const testKey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + +// tsFixtures are ciphertexts produced by the TypeScript @ctrlplane/secrets +// package using testKey. Any drift from byte-level interop will break these. +var tsFixtures = []struct { + plaintext string + ciphertext string +}{ + { + plaintext: "hello", + ciphertext: "c72e33c634ce0cb81e16e2dea3ae8ece:b8d32739794be620178e9b0c8b314969", + }, + { + plaintext: "", + ciphertext: "a47023812378a4bf2e3986f931cd7a6d:3ad36d456286bc05ad86a354b0cb6822", + }, + { + plaintext: "a longer plaintext with spaces and punctuation, including 0123456789!", + ciphertext: "bbd1fb174926cf4d5ee3c71a9020af37:748ddb2465eee4bd20829a37c974086b2ed5b68ec7f55d53beb7a91b1e42025aa4a58e35af5a303d7e5e7ec5ade14ad6708da09189c7c0150d515f9eb9d2d602c735fc7d8ecb3fd36a6f894f6174f12f", + }, + { + plaintext: `{"serviceToken":"dp.st.abcdef","region":"us-east-1"}`, + ciphertext: "4eddf95c6ed56c14b0a34c66468ffefb:fab54199d89ffc29df9c4ab62160538a4d2f8c4decfcdce8065a0a6334109562982f87e8d364c9b43eb8c54b45e98732d6853cce381f1b1a9b21cd8f1490f3e8", + }, +} + +func TestDecryptInteropWithTypeScript(t *testing.T) { + svc, err := New(testKey) + if err != nil { + t.Fatalf("New: %v", err) + } + for _, f := range tsFixtures { + got, err := svc.Decrypt(f.ciphertext) + if err != nil { + t.Fatalf("Decrypt(%q): %v", f.ciphertext, err) + } + if got != f.plaintext { + t.Fatalf("Decrypt(%q): want %q, got %q", f.ciphertext, f.plaintext, got) + } + } +} + +func TestRoundTrip(t *testing.T) { + svc, err := New(testKey) + if err != nil { + t.Fatalf("New: %v", err) + } + cases := []string{ + "", + "x", + "hello", + strings.Repeat("a", 16), + strings.Repeat("b", 17), + strings.Repeat("c", 31), + strings.Repeat("d", 32), + `{"key":"value","nested":{"a":1}}`, + } + for _, c := range cases { + ct, err := svc.Encrypt(c) + if err != nil { + t.Fatalf("Encrypt(%q): %v", c, err) + } + if !strings.Contains(ct, ":") { + t.Fatalf("Encrypt(%q): ciphertext missing iv separator: %s", c, ct) + } + pt, err := svc.Decrypt(ct) + if err != nil { + t.Fatalf("Decrypt(%q): %v", ct, err) + } + if pt != c { + t.Fatalf("round trip: want %q, got %q", c, pt) + } + } +} + +func TestEncryptUniqueIV(t *testing.T) { + svc, err := New(testKey) + if err != nil { + t.Fatalf("New: %v", err) + } + ct1, err := svc.Encrypt("same plaintext") + if err != nil { + t.Fatalf("Encrypt: %v", err) + } + ct2, err := svc.Encrypt("same plaintext") + if err != nil { + t.Fatalf("Encrypt: %v", err) + } + if ct1 == ct2 { + t.Fatal( + "two encryptions of the same plaintext produced identical ciphertext (IV not random)", + ) + } +} + +func TestNewRejectsBadKey(t *testing.T) { + cases := []string{ + "", + "too short", + strings.Repeat("z", 64), // not hex + strings.Repeat("a", 63), + strings.Repeat("a", 65), + } + for _, c := range cases { + if _, err := New(c); err == nil { + t.Fatalf("New(%q) expected error", c) + } + } +} + +func TestDecryptRejectsMalformed(t *testing.T) { + svc, err := New(testKey) + if err != nil { + t.Fatalf("New: %v", err) + } + cases := []string{ + "", + "no-separator", + "deadbeef", + ":onlysepatstart", + "abc:", + "zz:zz", + "00112233445566778899aabbccddeeff:zz", + } + for _, c := range cases { + if _, err := svc.Decrypt(c); err == nil { + t.Fatalf("Decrypt(%q) expected error", c) + } + } +} diff --git a/packages/db/drizzle/0195_unique_cobalt_man.sql b/packages/db/drizzle/0195_unique_cobalt_man.sql new file mode 100644 index 000000000..6b8443a6f --- /dev/null +++ b/packages/db/drizzle/0195_unique_cobalt_man.sql @@ -0,0 +1,13 @@ +CREATE TYPE "public"."secret_provider_type" AS ENUM('aws_secrets_manager', 'doppler', 'env');--> statement-breakpoint +CREATE TABLE "secret_provider" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "workspace_id" uuid NOT NULL, + "name" text NOT NULL, + "type" "secret_provider_type" NOT NULL, + "config" "bytea" NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "secret_provider" ADD CONSTRAINT "secret_provider_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "secret_provider_workspace_name_uniq" ON "secret_provider" USING btree ("workspace_id","name"); \ No newline at end of file diff --git a/packages/db/drizzle/meta/0195_snapshot.json b/packages/db/drizzle/meta/0195_snapshot.json new file mode 100644 index 000000000..e6c0c9326 --- /dev/null +++ b/packages/db/drizzle/meta/0195_snapshot.json @@ -0,0 +1,7607 @@ +{ + "id": "3efa7aef-401a-4c31-ad08-524dbe24d098", + "prevId": "9fdb978a-cc18-471a-b71f-6818a5f0f4db", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "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 + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "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 + }, + "password": { + "name": "password", + "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": true, + "default": "now()" + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "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": true + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_session_token_unique": { + "name": "session_session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "session_token" + ] + } + }, + "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 + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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()" + }, + "updated_at": { + "name": "updated_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.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.changelog_entry": { + "name": "changelog_entry", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_data": { + "name": "entity_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "changelog_entry_workspace_id_workspace_id_fk": { + "name": "changelog_entry_workspace_id_workspace_id_fk", + "tableFrom": "changelog_entry", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "changelog_entry_workspace_id_entity_type_entity_id_pk": { + "name": "changelog_entry_workspace_id_entity_type_entity_id_pk", + "columns": [ + "workspace_id", + "entity_type", + "entity_id" + ] + } + }, + "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_plan": { + "name": "deployment_plan", + "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 + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version_tag": { + "name": "version_tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version_name": { + "name": "version_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version_config": { + "name": "version_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "version_job_agent_config": { + "name": "version_job_agent_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "version_metadata": { + "name": "version_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "deployment_plan_workspace_id_index": { + "name": "deployment_plan_workspace_id_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_plan_deployment_id_index": { + "name": "deployment_plan_deployment_id_index", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_plan_expires_at_index": { + "name": "deployment_plan_expires_at_index", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_plan_workspace_id_workspace_id_fk": { + "name": "deployment_plan_workspace_id_workspace_id_fk", + "tableFrom": "deployment_plan", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_plan_deployment_id_deployment_id_fk": { + "name": "deployment_plan_deployment_id_deployment_id_fk", + "tableFrom": "deployment_plan", + "tableTo": "deployment", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_plan_target": { + "name": "deployment_plan_target", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plan_id": { + "name": "plan_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "current_release_id": { + "name": "current_release_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "deployment_plan_target_plan_id_index": { + "name": "deployment_plan_target_plan_id_index", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_plan_target_plan_id_environment_id_resource_id_index": { + "name": "deployment_plan_target_plan_id_environment_id_resource_id_index", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_plan_target_plan_id_deployment_plan_id_fk": { + "name": "deployment_plan_target_plan_id_deployment_plan_id_fk", + "tableFrom": "deployment_plan_target", + "tableTo": "deployment_plan", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_plan_target_environment_id_environment_id_fk": { + "name": "deployment_plan_target_environment_id_environment_id_fk", + "tableFrom": "deployment_plan_target", + "tableTo": "environment", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_plan_target_resource_id_resource_id_fk": { + "name": "deployment_plan_target_resource_id_resource_id_fk", + "tableFrom": "deployment_plan_target", + "tableTo": "resource", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_plan_target_current_release_id_release_id_fk": { + "name": "deployment_plan_target_current_release_id_release_id_fk", + "tableFrom": "deployment_plan_target", + "tableTo": "release", + "columnsFrom": [ + "current_release_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_plan_target_result": { + "name": "deployment_plan_target_result", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "dispatch_context": { + "name": "dispatch_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "deployment_plan_target_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'computing'" + }, + "has_changes": { + "name": "has_changes", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current": { + "name": "current", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proposed": { + "name": "proposed", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "deployment_plan_target_result_target_id_index": { + "name": "deployment_plan_target_result_target_id_index", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_plan_target_result_target_id_deployment_plan_target_id_fk": { + "name": "deployment_plan_target_result_target_id_deployment_plan_target_id_fk", + "tableFrom": "deployment_plan_target_result", + "tableTo": "deployment_plan_target", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_plan_target_result_validation": { + "name": "deployment_plan_target_result_validation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "result_id": { + "name": "result_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "passed": { + "name": "passed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "violations": { + "name": "violations", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "evaluated_at": { + "name": "evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "deployment_plan_target_result_validation_result_id_rule_id_index": { + "name": "deployment_plan_target_result_validation_result_id_rule_id_index", + "columns": [ + { + "expression": "result_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_plan_target_result_validation_result_id_deployment_plan_target_result_id_fk": { + "name": "deployment_plan_target_result_validation_result_id_deployment_plan_target_result_id_fk", + "tableFrom": "deployment_plan_target_result_validation", + "tableTo": "deployment_plan_target_result", + "columnsFrom": [ + "result_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_plan_target_variable": { + "name": "deployment_plan_target_variable", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_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 + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "deployment_plan_target_variable_target_id_key_index": { + "name": "deployment_plan_target_variable_target_id_key_index", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_plan_target_variable_target_id_deployment_plan_target_id_fk": { + "name": "deployment_plan_target_variable_target_id_deployment_plan_target_id_fk", + "tableFrom": "deployment_plan_target_variable", + "tableTo": "deployment_plan_target", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_trace_span": { + "name": "deployment_trace_span", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "span_id": { + "name": "span_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_span_id": { + "name": "parent_span_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_time": { + "name": "start_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_time": { + "name": "end_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "release_target_key": { + "name": "release_target_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_id": { + "name": "release_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_trace_id": { + "name": "parent_trace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "node_type": { + "name": "node_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "depth": { + "name": "depth", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attributes": { + "name": "attributes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "events": { + "name": "events", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "deployment_trace_span_trace_span_idx": { + "name": "deployment_trace_span_trace_span_idx", + "columns": [ + { + "expression": "trace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "span_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_trace_id_idx": { + "name": "deployment_trace_span_trace_id_idx", + "columns": [ + { + "expression": "trace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_parent_span_id_idx": { + "name": "deployment_trace_span_parent_span_id_idx", + "columns": [ + { + "expression": "parent_span_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_workspace_id_idx": { + "name": "deployment_trace_span_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_release_target_key_idx": { + "name": "deployment_trace_span_release_target_key_idx", + "columns": [ + { + "expression": "release_target_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_release_id_idx": { + "name": "deployment_trace_span_release_id_idx", + "columns": [ + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_job_id_idx": { + "name": "deployment_trace_span_job_id_idx", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_parent_trace_id_idx": { + "name": "deployment_trace_span_parent_trace_id_idx", + "columns": [ + { + "expression": "parent_trace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_created_at_idx": { + "name": "deployment_trace_span_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_phase_idx": { + "name": "deployment_trace_span_phase_idx", + "columns": [ + { + "expression": "phase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_node_type_idx": { + "name": "deployment_trace_span_node_type_idx", + "columns": [ + { + "expression": "node_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_status_idx": { + "name": "deployment_trace_span_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_trace_span_workspace_id_workspace_id_fk": { + "name": "deployment_trace_span_workspace_id_workspace_id_fk", + "tableFrom": "deployment_trace_span", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_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()" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_value": { + "name": "default_value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "deployment_variable_deployment_id_index": { + "name": "deployment_variable_deployment_id_index", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "deployment_variable_deployment_id_key_unique": { + "name": "deployment_variable_deployment_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "deployment_id", + "key" + ] + } + }, + "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()" + }, + "deployment_variable_id": { + "name": "deployment_variable_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "resource_selector": { + "name": "resource_selector", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "deployment_variable_value_deployment_variable_id_index": { + "name": "deployment_variable_value_deployment_variable_id_index", + "columns": [ + { + "expression": "deployment_variable_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_variable_value_deployment_variable_id_deployment_variable_id_fk": { + "name": "deployment_variable_value_deployment_variable_id_deployment_variable_id_fk", + "tableFrom": "deployment_variable_value", + "tableTo": "deployment_variable", + "columnsFrom": [ + "deployment_variable_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "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_workspace_id_workspace_id_fk": { + "name": "deployment_version_workspace_id_workspace_id_fk", + "tableFrom": "deployment_version", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_version_dependency": { + "name": "deployment_version_dependency", + "schema": "", + "columns": { + "deployment_version_id": { + "name": "deployment_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "dependency_deployment_id": { + "name": "dependency_deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version_selector": { + "name": "version_selector", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'false'" + } + }, + "indexes": { + "deployment_version_dependency_target_idx": { + "name": "deployment_version_dependency_target_idx", + "columns": [ + { + "expression": "dependency_deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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_dependency_deployment_id_deployment_id_fk": { + "name": "deployment_version_dependency_dependency_deployment_id_deployment_id_fk", + "tableFrom": "deployment_version_dependency", + "tableTo": "deployment", + "columnsFrom": [ + "dependency_deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "deployment_version_dependency_deployment_version_id_dependency_deployment_id_pk": { + "name": "deployment_version_dependency_deployment_version_id_dependency_deployment_id_pk", + "columns": [ + "deployment_version_id", + "dependency_deployment_id" + ] + } + }, + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_evaluated_at": { + "name": "last_evaluated_at", + "type": "timestamp with time zone", + "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 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_selector": { + "name": "resource_selector", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'false'" + }, + "job_agent_selector": { + "name": "job_agent_selector", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'false'" + }, + "job_agent_config": { + "name": "job_agent_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "deployment_workspace_id_workspace_id_fk": { + "name": "deployment_workspace_id_workspace_id_fk", + "tableFrom": "deployment", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "deployment_workspace_id_name_unique": { + "name": "deployment_workspace_id_name_unique", + "nullsNotDistinct": false, + "columns": [ + "workspace_id", + "name" + ] + } + }, + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_evaluated_at": { + "name": "last_evaluated_at", + "type": "timestamp with time zone", + "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()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "resource_selector": { + "name": "resource_selector", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'false'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "environment_workspace_id_workspace_id_fk": { + "name": "environment_workspace_id_workspace_id_fk", + "tableFrom": "environment", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_workspace_id_name_unique": { + "name": "environment_workspace_id_name_unique", + "nullsNotDistinct": false, + "columns": [ + "workspace_id", + "name" + ] + } + }, + "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.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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "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": {} + }, + "resource_workspace_id_active_idx": { + "name": "resource_workspace_id_active_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resource_workspace_id_deleted_at_index": { + "name": "resource_workspace_id_deleted_at_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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_aggregate": { + "name": "resource_aggregate", + "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": 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 + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "filter": { + "name": "filter", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'true'" + }, + "group_by": { + "name": "group_by", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resource_aggregate_workspace_id_index": { + "name": "resource_aggregate_workspace_id_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_aggregate_workspace_id_workspace_id_fk": { + "name": "resource_aggregate_workspace_id_workspace_id_fk", + "tableFrom": "resource_aggregate", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "resource_aggregate_created_by_user_id_fk": { + "name": "resource_aggregate_created_by_user_id_fk", + "tableFrom": "resource_aggregate", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "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_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 with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "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": "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 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": { + "system_workspace_id_index": { + "name": "system_workspace_id_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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.system_deployment": { + "name": "system_deployment", + "schema": "", + "columns": { + "system_id": { + "name": "system_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "deployment_id": { + "name": "deployment_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": { + "system_deployment_system_id_system_id_fk": { + "name": "system_deployment_system_id_system_id_fk", + "tableFrom": "system_deployment", + "tableTo": "system", + "columnsFrom": [ + "system_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "system_deployment_deployment_id_deployment_id_fk": { + "name": "system_deployment_deployment_id_deployment_id_fk", + "tableFrom": "system_deployment", + "tableTo": "deployment", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "system_deployment_system_id_deployment_id_pk": { + "name": "system_deployment_system_id_deployment_id_pk", + "columns": [ + "system_id", + "deployment_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_environment": { + "name": "system_environment", + "schema": "", + "columns": { + "system_id": { + "name": "system_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_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": { + "system_environment_system_id_system_id_fk": { + "name": "system_environment_system_id_system_id_fk", + "tableFrom": "system_environment", + "tableTo": "system", + "columnsFrom": [ + "system_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "system_environment_environment_id_environment_id_fk": { + "name": "system_environment_environment_id_environment_id_fk", + "tableFrom": "system_environment", + "tableTo": "environment", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "system_environment_system_id_environment_id_pk": { + "name": "system_environment_system_id_environment_id_pk", + "columns": [ + "system_id", + "environment_id" + ] + } + }, + "uniqueConstraints": {}, + "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 + }, + "trace_token": { + "name": "trace_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dispatch_context": { + "name": "dispatch_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "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": {} + }, + "job_external_id_idx": { + "name": "job_external_id_idx", + "columns": [ + { + "expression": "external_id", + "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": {} + }, + "job_metadata_job_id_idx": { + "name": "job_metadata_job_id_idx", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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 + }, + "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.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.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.release": { + "name": "release", + "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 + }, + "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": { + "release_resource_id_environment_id_deployment_id_index": { + "name": "release_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": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "release_deployment_id_index": { + "name": "release_deployment_id_index", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "release_resource_id_resource_id_fk": { + "name": "release_resource_id_resource_id_fk", + "tableFrom": "release", + "tableTo": "resource", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_environment_id_environment_id_fk": { + "name": "release_environment_id_environment_id_fk", + "tableFrom": "release", + "tableTo": "environment", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_deployment_id_deployment_id_fk": { + "name": "release_deployment_id_deployment_id_fk", + "tableFrom": "release", + "tableTo": "deployment", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_version_id_deployment_version_id_fk": { + "name": "release_version_id_deployment_version_id_fk", + "tableFrom": "release", + "tableTo": "deployment_version", + "columnsFrom": [ + "version_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()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "release_id": { + "name": "release_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "release_job_release_id_job_id_index": { + "name": "release_job_release_id_job_id_index", + "columns": [ + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "release_job_job_id_index": { + "name": "release_job_job_id_index", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "release_job_release_id_index": { + "name": "release_job_release_id_index", + "columns": [ + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "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" + }, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.release_target_desired_release": { + "name": "release_target_desired_release", + "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 + } + }, + "indexes": { + "release_target_desired_release_resource_id_environment_id_deployment_id_index": { + "name": "release_target_desired_release_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_desired_release_resource_id_resource_id_fk": { + "name": "release_target_desired_release_resource_id_resource_id_fk", + "tableFrom": "release_target_desired_release", + "tableTo": "resource", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_target_desired_release_environment_id_environment_id_fk": { + "name": "release_target_desired_release_environment_id_environment_id_fk", + "tableFrom": "release_target_desired_release", + "tableTo": "environment", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_target_desired_release_deployment_id_deployment_id_fk": { + "name": "release_target_desired_release_deployment_id_deployment_id_fk", + "tableFrom": "release_target_desired_release", + "tableTo": "deployment", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_target_desired_release_desired_release_id_release_id_fk": { + "name": "release_target_desired_release_desired_release_id_release_id_fk", + "tableFrom": "release_target_desired_release", + "tableTo": "release", + "columnsFrom": [ + "desired_release_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.release_variable": { + "name": "release_variable", + "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 + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "encrypted": { + "name": "encrypted", + "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": { + "release_variable_release_id_key_index": { + "name": "release_variable_release_id_key_index", + "columns": [ + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "release_variable_release_id_release_id_fk": { + "name": "release_variable_release_id_release_id_fk", + "tableFrom": "release_variable", + "tableTo": "release", + "columnsFrom": [ + "release_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reconcile_work_scope": { + "name": "reconcile_work_scope", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "reconcile_work_scope_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "event_ts": { + "name": "event_ts", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "priority": { + "name": "priority", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "not_before": { + "name": "not_before", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_until": { + "name": "claimed_until", + "type": "timestamp with time zone", + "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": true, + "default": "now()" + } + }, + "indexes": { + "reconcile_work_scope_workspace_id_kind_scope_type_scope_id_index": { + "name": "reconcile_work_scope_workspace_id_kind_scope_type_scope_id_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "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": {} + }, + "reconcile_work_scope_unclaimed_idx": { + "name": "reconcile_work_scope_unclaimed_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_ts", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"reconcile_work_scope\".\"claimed_until\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "reconcile_work_scope_expired_claims_idx": { + "name": "reconcile_work_scope_expired_claims_idx", + "columns": [ + { + "expression": "claimed_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"reconcile_work_scope\".\"claimed_until\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "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 + }, + "selector": { + "name": "selector", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'true'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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()" + } + }, + "indexes": { + "policy_workspace_id_index": { + "name": "policy_workspace_id_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": {}, + "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 + }, + "min_approvals": { + "name": "min_approvals", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "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_deployment_dependency": { + "name": "policy_rule_deployment_dependency", + "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 + }, + "depends_on": { + "name": "depends_on", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_deployment_dependency_policy_id_policy_id_fk": { + "name": "policy_rule_deployment_dependency_policy_id_policy_id_fk", + "tableFrom": "policy_rule_deployment_dependency", + "tableTo": "policy", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_deployment_window": { + "name": "policy_rule_deployment_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 + }, + "allow_window": { + "name": "allow_window", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "duration_minutes": { + "name": "duration_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rrule": { + "name": "rrule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_deployment_window_policy_id_policy_id_fk": { + "name": "policy_rule_deployment_window_policy_id_policy_id_fk", + "tableFrom": "policy_rule_deployment_window", + "tableTo": "policy", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_environment_progression": { + "name": "policy_rule_environment_progression", + "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 + }, + "depends_on_environment_selector": { + "name": "depends_on_environment_selector", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "maximum_age_hours": { + "name": "maximum_age_hours", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "minimum_soak_time_minutes": { + "name": "minimum_soak_time_minutes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "minimum_success_percentage": { + "name": "minimum_success_percentage", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "success_statuses": { + "name": "success_statuses", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "require_verification_passed": { + "name": "require_verification_passed", + "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": {}, + "foreignKeys": { + "policy_rule_environment_progression_policy_id_policy_id_fk": { + "name": "policy_rule_environment_progression_policy_id_policy_id_fk", + "tableFrom": "policy_rule_environment_progression", + "tableTo": "policy", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_gradual_rollout": { + "name": "policy_rule_gradual_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 + }, + "rollout_type": { + "name": "rollout_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time_scale_interval": { + "name": "time_scale_interval", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_gradual_rollout_policy_id_policy_id_fk": { + "name": "policy_rule_gradual_rollout_policy_id_policy_id_fk", + "tableFrom": "policy_rule_gradual_rollout", + "tableTo": "policy", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_plan_validation_opa": { + "name": "policy_rule_plan_validation_opa", + "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 + }, + "rego": { + "name": "rego", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "policy_rule_plan_validation_opa_policy_id_index": { + "name": "policy_rule_plan_validation_opa_policy_id_index", + "columns": [ + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "policy_rule_plan_validation_opa_policy_id_policy_id_fk": { + "name": "policy_rule_plan_validation_opa_policy_id_policy_id_fk", + "tableFrom": "policy_rule_plan_validation_opa", + "tableTo": "policy", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_retry": { + "name": "policy_rule_retry", + "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 + }, + "max_retries": { + "name": "max_retries", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "backoff_seconds": { + "name": "backoff_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "backoff_strategy": { + "name": "backoff_strategy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_backoff_seconds": { + "name": "max_backoff_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "retry_on_statuses": { + "name": "retry_on_statuses", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_retry_policy_id_policy_id_fk": { + "name": "policy_rule_retry_policy_id_policy_id_fk", + "tableFrom": "policy_rule_retry", + "tableTo": "policy", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_rollback": { + "name": "policy_rule_rollback", + "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 + }, + "on_job_statuses": { + "name": "on_job_statuses", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "on_verification_failure": { + "name": "on_verification_failure", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_rollback_policy_id_policy_id_fk": { + "name": "policy_rule_rollback_policy_id_policy_id_fk", + "tableFrom": "policy_rule_rollback", + "tableTo": "policy", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_verification": { + "name": "policy_rule_verification", + "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 + }, + "metrics": { + "name": "metrics", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "trigger_on": { + "name": "trigger_on", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_verification_policy_id_policy_id_fk": { + "name": "policy_rule_verification_policy_id_policy_id_fk", + "tableFrom": "policy_rule_verification", + "tableTo": "policy", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_version_cooldown": { + "name": "policy_rule_version_cooldown", + "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 + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_version_cooldown_policy_id_policy_id_fk": { + "name": "policy_rule_version_cooldown_policy_id_policy_id_fk", + "tableFrom": "policy_rule_version_cooldown", + "tableTo": "policy", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_version_selector": { + "name": "policy_rule_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 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "selector": { + "name": "selector", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_version_selector_policy_id_policy_id_fk": { + "name": "policy_rule_version_selector_policy_id_policy_id_fk", + "tableFrom": "policy_rule_version_selector", + "tableTo": "policy", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_approval_record": { + "name": "user_approval_record", + "schema": "", + "columns": { + "version_id": { + "name": "version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_approval_record_version_id_user_id_environment_id_pk": { + "name": "user_approval_record_version_id_user_id_environment_id_pk", + "columns": [ + "version_id", + "user_id", + "environment_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_variable": { + "name": "resource_variable", + "schema": "", + "columns": { + "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": true + } + }, + "indexes": {}, + "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": { + "resource_variable_resource_id_key_pk": { + "name": "resource_variable_resource_id_key_pk", + "columns": [ + "resource_id", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "job_agents": { + "name": "job_agents", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_job": { + "name": "workflow_job", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workflow_run_id": { + "name": "workflow_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_job_workflow_run_id_workflow_run_id_fk": { + "name": "workflow_job_workflow_run_id_workflow_run_id_fk", + "tableFrom": "workflow_job", + "tableTo": "workflow_run", + "columnsFrom": [ + "workflow_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_job_job_id_job_id_fk": { + "name": "workflow_job_job_id_job_id_fk", + "tableFrom": "workflow_job", + "tableTo": "job", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_run": { + "name": "workflow_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workflow_id": { + "name": "workflow_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_run_workflow_id_workflow_id_fk": { + "name": "workflow_run_workflow_id_workflow_id_fk", + "tableFrom": "workflow_run", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_skip": { + "name": "policy_skip", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "rule_id": { + "name": "rule_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version_id": { + "name": "version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.computed_policy_release_target": { + "name": "computed_policy_release_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 + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "computed_at": { + "name": "computed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "computed_policy_release_target_policy_id_environment_id_deployment_id_resource_id_index": { + "name": "computed_policy_release_target_policy_id_environment_id_deployment_id_resource_id_index", + "columns": [ + { + "expression": "policy_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" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "computed_policy_release_target_policy_id_index": { + "name": "computed_policy_release_target_policy_id_index", + "columns": [ + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "computed_policy_release_target_resource_id_environment_id_deployment_id_index": { + "name": "computed_policy_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": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "computed_policy_release_target_policy_id_policy_id_fk": { + "name": "computed_policy_release_target_policy_id_policy_id_fk", + "tableFrom": "computed_policy_release_target", + "tableTo": "policy", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "computed_policy_release_target_environment_id_environment_id_fk": { + "name": "computed_policy_release_target_environment_id_environment_id_fk", + "tableFrom": "computed_policy_release_target", + "tableTo": "environment", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "computed_policy_release_target_deployment_id_deployment_id_fk": { + "name": "computed_policy_release_target_deployment_id_deployment_id_fk", + "tableFrom": "computed_policy_release_target", + "tableTo": "deployment", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "computed_policy_release_target_resource_id_resource_id_fk": { + "name": "computed_policy_release_target_resource_id_resource_id_fk", + "tableFrom": "computed_policy_release_target", + "tableTo": "resource", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_evaluation": { + "name": "policy_rule_evaluation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "rule_type": { + "name": "rule_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version_id": { + "name": "version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "allowed": { + "name": "allowed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "action_required": { + "name": "action_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "satisfied_at": { + "name": "satisfied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_evaluation_at": { + "name": "next_evaluation_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "evaluated_at": { + "name": "evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "policy_rule_evaluation_rule_id_environment_id_version_id_resource_id_index": { + "name": "policy_rule_evaluation_rule_id_environment_id_version_id_resource_id_index", + "columns": [ + { + "expression": "rule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "policy_rule_evaluation_environment_id_version_id_resource_id_rule_type_index": { + "name": "policy_rule_evaluation_environment_id_version_id_resource_id_rule_type_index", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rule_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "policy_rule_evaluation_environment_id_environment_id_fk": { + "name": "policy_rule_evaluation_environment_id_environment_id_fk", + "tableFrom": "policy_rule_evaluation", + "tableTo": "environment", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "policy_rule_evaluation_version_id_deployment_version_id_fk": { + "name": "policy_rule_evaluation_version_id_deployment_version_id_fk", + "tableFrom": "policy_rule_evaluation", + "tableTo": "deployment_version", + "columnsFrom": [ + "version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "policy_rule_evaluation_resource_id_resource_id_fk": { + "name": "policy_rule_evaluation_resource_id_resource_id_fk", + "tableFrom": "policy_rule_evaluation", + "tableTo": "resource", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_verification_metric_measurement": { + "name": "job_verification_metric_measurement", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_verification_metric_status_id": { + "name": "job_verification_metric_status_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "measured_at": { + "name": "measured_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "job_verification_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "job_verification_metric_measurement_job_verification_metric_status_id_index": { + "name": "job_verification_metric_measurement_job_verification_metric_status_id_index", + "columns": [ + { + "expression": "job_verification_metric_status_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_verification_metric_measurement_job_verification_metric_status_id_job_verification_metric_id_fk": { + "name": "job_verification_metric_measurement_job_verification_metric_status_id_job_verification_metric_id_fk", + "tableFrom": "job_verification_metric_measurement", + "tableTo": "job_verification_metric", + "columnsFrom": [ + "job_verification_metric_status_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_verification_metric": { + "name": "job_verification_metric", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "policy_rule_verification_metric_id": { + "name": "policy_rule_verification_metric_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "success_threshold": { + "name": "success_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "failure_condition": { + "name": "failure_condition", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'false'" + }, + "failure_threshold": { + "name": "failure_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + } + }, + "indexes": { + "job_verification_metric_job_id_index": { + "name": "job_verification_metric_job_id_index", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_verification_metric_policy_rule_verification_metric_id_index": { + "name": "job_verification_metric_policy_rule_verification_metric_id_index", + "columns": [ + { + "expression": "policy_rule_verification_metric_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_verification_metric_policy_rule_verification_metric_id_policy_rule_job_verification_metric_id_fk": { + "name": "job_verification_metric_policy_rule_verification_metric_id_policy_rule_job_verification_metric_id_fk", + "tableFrom": "job_verification_metric", + "tableTo": "policy_rule_job_verification_metric", + "columnsFrom": [ + "policy_rule_verification_metric_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_job_verification_metric": { + "name": "policy_rule_job_verification_metric", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "trigger_on": { + "name": "trigger_on", + "type": "job_verification_trigger_on", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'jobSuccess'" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "success_threshold": { + "name": "success_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "failure_condition": { + "name": "failure_condition", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'false'" + }, + "failure_threshold": { + "name": "failure_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_job_verification_metric_policy_id_policy_id_fk": { + "name": "policy_rule_job_verification_metric_policy_id_policy_id_fk", + "tableFrom": "policy_rule_job_verification_metric", + "tableTo": "policy", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.computed_entity_relationship": { + "name": "computed_entity_relationship", + "schema": "", + "columns": { + "rule_id": { + "name": "rule_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "from_entity_type": { + "name": "from_entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_entity_id": { + "name": "from_entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "to_entity_type": { + "name": "to_entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_entity_id": { + "name": "to_entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "last_evaluated_at": { + "name": "last_evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "computed_entity_relationship_from_idx": { + "name": "computed_entity_relationship_from_idx", + "columns": [ + { + "expression": "from_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "from_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "computed_entity_relationship_to_idx": { + "name": "computed_entity_relationship_to_idx", + "columns": [ + { + "expression": "to_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "to_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "computed_entity_relationship_rule_id_relationship_rule_id_fk": { + "name": "computed_entity_relationship_rule_id_relationship_rule_id_fk", + "tableFrom": "computed_entity_relationship", + "tableTo": "relationship_rule", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "computed_entity_relationship_rule_id_from_entity_type_from_entity_id_to_entity_type_to_entity_id_pk": { + "name": "computed_entity_relationship_rule_id_from_entity_type_from_entity_id_to_entity_type_to_entity_id_pk", + "columns": [ + "rule_id", + "from_entity_type", + "from_entity_id", + "to_entity_type", + "to_entity_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.relationship_rule": { + "name": "relationship_rule", + "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": true + }, + "reference": { + "name": "reference", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cel": { + "name": "cel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + } + }, + "indexes": { + "relationship_rule_workspace_id_reference_index": { + "name": "relationship_rule_workspace_id_reference_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reference", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "relationship_rule_workspace_id_index": { + "name": "relationship_rule_workspace_id_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "relationship_rule_workspace_id_workspace_id_fk": { + "name": "relationship_rule_workspace_id_workspace_id_fk", + "tableFrom": "relationship_rule", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_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": "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": true, + "default": "''" + }, + "selector": { + "name": "selector", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "variable_set_workspace_id_workspace_id_fk": { + "name": "variable_set_workspace_id_workspace_id_fk", + "tableFrom": "variable_set", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.variable_set_variable": { + "name": "variable_set_variable", + "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 + } + }, + "indexes": {}, + "foreignKeys": { + "variable_set_variable_variable_set_id_variable_set_id_fk": { + "name": "variable_set_variable_variable_set_id_variable_set_id_fk", + "tableFrom": "variable_set_variable", + "tableTo": "variable_set", + "columnsFrom": [ + "variable_set_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "variable_set_variable_variable_set_id_key_unique": { + "name": "variable_set_variable_variable_set_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "variable_set_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.variable": { + "name": "variable", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scope": { + "name": "scope", + "type": "variable_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "job_agent_id": { + "name": "job_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_sensitive": { + "name": "is_sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "description": { + "name": "description", + "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": true, + "default": "now()" + } + }, + "indexes": { + "variable_resource_key_uniq": { + "name": "variable_resource_key_uniq", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"variable\".\"resource_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "variable_deployment_key_uniq": { + "name": "variable_deployment_key_uniq", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"variable\".\"deployment_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "variable_job_agent_key_uniq": { + "name": "variable_job_agent_key_uniq", + "columns": [ + { + "expression": "job_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"variable\".\"job_agent_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "variable_scope_idx": { + "name": "variable_scope_idx", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "variable_resource_id_resource_id_fk": { + "name": "variable_resource_id_resource_id_fk", + "tableFrom": "variable", + "tableTo": "resource", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "variable_deployment_id_deployment_id_fk": { + "name": "variable_deployment_id_deployment_id_fk", + "tableFrom": "variable", + "tableTo": "deployment", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "variable_job_agent_id_job_agent_id_fk": { + "name": "variable_job_agent_id_job_agent_id_fk", + "tableFrom": "variable", + "tableTo": "job_agent", + "columnsFrom": [ + "job_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "variable_scope_target_check": { + "name": "variable_scope_target_check", + "value": "\n (\n \"variable\".\"scope\" = 'resource'\n and \"variable\".\"resource_id\" is not null\n and \"variable\".\"deployment_id\" is null\n and \"variable\".\"job_agent_id\" is null\n )\n or\n (\n \"variable\".\"scope\" = 'deployment'\n and \"variable\".\"deployment_id\" is not null\n and \"variable\".\"resource_id\" is null\n and \"variable\".\"job_agent_id\" is null\n )\n or\n (\n \"variable\".\"scope\" = 'job_agent'\n and \"variable\".\"job_agent_id\" is not null\n and \"variable\".\"resource_id\" is null\n and \"variable\".\"deployment_id\" is null\n )\n " + } + }, + "isRLSEnabled": false + }, + "public.variable_value": { + "name": "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": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "kind": { + "name": "kind", + "type": "variable_value_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "literal_value": { + "name": "literal_value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ref_key": { + "name": "ref_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ref_path": { + "name": "ref_path", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "secret_provider": { + "name": "secret_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_key": { + "name": "secret_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_path": { + "name": "secret_path", + "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": true, + "default": "now()" + } + }, + "indexes": { + "variable_value_variable_priority_idx": { + "name": "variable_value_variable_priority_idx", + "columns": [ + { + "expression": "variable_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "variable_value_kind_idx": { + "name": "variable_value_kind_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "variable_value_resolution_uniq": { + "name": "variable_value_resolution_uniq", + "columns": [ + { + "expression": "variable_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"resource_selector\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "variable_value_variable_id_variable_id_fk": { + "name": "variable_value_variable_id_variable_id_fk", + "tableFrom": "variable_value", + "tableTo": "variable", + "columnsFrom": [ + "variable_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "variable_value_kind_shape_check": { + "name": "variable_value_kind_shape_check", + "value": "\n (\n \"variable_value\".\"kind\" = 'literal'\n and \"variable_value\".\"literal_value\" is not null\n and \"variable_value\".\"ref_key\" is null\n and \"variable_value\".\"ref_path\" is null\n and \"variable_value\".\"secret_provider\" is null\n and \"variable_value\".\"secret_key\" is null\n and \"variable_value\".\"secret_path\" is null\n )\n or\n (\n \"variable_value\".\"kind\" = 'ref'\n and \"variable_value\".\"literal_value\" is null\n and \"variable_value\".\"ref_key\" is not null\n and \"variable_value\".\"secret_provider\" is null\n and \"variable_value\".\"secret_key\" is null\n and \"variable_value\".\"secret_path\" is null\n )\n or\n (\n \"variable_value\".\"kind\" = 'secret_ref'\n and \"variable_value\".\"literal_value\" is null\n and \"variable_value\".\"ref_key\" is null\n and \"variable_value\".\"ref_path\" is null\n and \"variable_value\".\"secret_provider\" is not null\n and \"variable_value\".\"secret_key\" is not null\n )\n " + } + }, + "isRLSEnabled": false + }, + "public.secret_provider": { + "name": "secret_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 + }, + "type": { + "name": "type", + "type": "secret_provider_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "bytea", + "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": true, + "default": "now()" + } + }, + "indexes": { + "secret_provider_workspace_name_uniq": { + "name": "secret_provider_workspace_name_uniq", + "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": { + "secret_provider_workspace_id_workspace_id_fk": { + "name": "secret_provider_workspace_id_workspace_id_fk", + "tableFrom": "secret_provider", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_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_plan_target_status": { + "name": "deployment_plan_target_status", + "schema": "public", + "values": [ + "computing", + "completed", + "errored", + "unsupported" + ] + }, + "public.deployment_version_status": { + "name": "deployment_version_status", + "schema": "public", + "values": [ + "unspecified", + "building", + "ready", + "failed", + "rejected", + "paused" + ] + }, + "public.job_reason": { + "name": "job_reason", + "schema": "public", + "values": [ + "policy_passing", + "policy_override", + "env_policy_override", + "config_policy_override", + "redeploy" + ] + }, + "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", + "resource", + "resourceProvider", + "workspace", + "environment", + "system", + "deployment" + ] + }, + "public.job_verification_status": { + "name": "job_verification_status", + "schema": "public", + "values": [ + "failed", + "inconclusive", + "passed" + ] + }, + "public.job_verification_trigger_on": { + "name": "job_verification_trigger_on", + "schema": "public", + "values": [ + "jobCreated", + "jobStarted", + "jobSuccess", + "jobFailure" + ] + }, + "public.variable_scope": { + "name": "variable_scope", + "schema": "public", + "values": [ + "resource", + "deployment", + "job_agent" + ] + }, + "public.variable_value_kind": { + "name": "variable_value_kind", + "schema": "public", + "values": [ + "literal", + "ref", + "secret_ref" + ] + }, + "public.secret_provider_type": { + "name": "secret_provider_type", + "schema": "public", + "values": [ + "aws_secrets_manager", + "doppler", + "env" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 02b63a45c..1b2af7ada 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -1366,6 +1366,13 @@ "when": 1777571750344, "tag": "0194_brief_longshot", "breakpoints": true + }, + { + "idx": 195, + "version": "7", + "when": 1778514718456, + "tag": "0195_unique_cobalt_man", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts index 3bd0d2481..b32dce637 100644 --- a/packages/db/src/schema/index.ts +++ b/packages/db/src/schema/index.ts @@ -30,3 +30,4 @@ export * from "./relationships.js"; export * from "./job-agent.js"; export * from "./variable-set.js"; export * from "./variable.js"; +export * from "./secret-provider.js"; diff --git a/packages/db/src/schema/secret-provider.ts b/packages/db/src/schema/secret-provider.ts new file mode 100644 index 000000000..72f013d5c --- /dev/null +++ b/packages/db/src/schema/secret-provider.ts @@ -0,0 +1,56 @@ +import type { InferSelectModel } from "drizzle-orm"; +import { customType } from "drizzle-orm/pg-core"; +import { + pgEnum, + pgTable, + text, + timestamp, + uniqueIndex, + uuid, +} from "drizzle-orm/pg-core"; + +import { workspace } from "./workspace.js"; + +export const secretProviderTypeEnum = pgEnum("secret_provider_type", [ + "aws_secrets_manager", + "doppler", + "env", +]); + +const bytea = customType<{ data: Buffer; driverData: Buffer }>({ + dataType: () => "bytea", +}); + +export const secretProvider = pgTable( + "secret_provider", + { + id: uuid("id").defaultRandom().primaryKey(), + + workspaceId: uuid("workspace_id") + .notNull() + .references(() => workspace.id, { onDelete: "cascade" }), + + name: text("name").notNull(), + + type: secretProviderTypeEnum("type").notNull(), + + config: bytea("config").notNull(), + + createdAt: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), + + updatedAt: timestamp("updated_at", { withTimezone: true }) + .notNull() + .defaultNow() + .$onUpdate(() => new Date()), + }, + (table) => [ + uniqueIndex("secret_provider_workspace_name_uniq").on( + table.workspaceId, + table.name, + ), + ], +); + +export type SecretProvider = InferSelectModel; From c54a00e5480a5fd6b3064205f60d4fc51883a1ab Mon Sep 17 00:00:00 2001 From: Michael Leone Date: Mon, 11 May 2026 12:13:12 -0400 Subject: [PATCH 02/13] feat(secret-providers): add REST CRUD endpoints for workspace secret providers - OpenAPI spec: new schemas (SecretProvider, SecretProviderType, per-type configs for aws_secrets_manager/doppler/env, UpsertSecretProviderRequest) and 4 routes under /v1/workspaces/{workspaceId}/secret-providers. SecretProvider responses intentionally omit the encrypted config blob. - apps/api: secret-providers.ts handler with Drizzle CRUD, a Zod discriminated union that enforces type<->config correspondence beyond what OpenAPI oneOf can express, and AES-256-CBC encrypt-on-write via @ctrlplane/secrets. Router mounted in v1/workspaces/index.ts. Added @ctrlplane/secrets to api deps. - e2e: secret-providers.spec.ts covers CRUD round trip, asserts the decrypted config is never returned on get or list, plus 404 / idempotent-upsert / validation-rejection cases. Plaintext configs are never persisted; encryption uses the shared VARIABLES_AES_256_KEY also consumed by the Go resolver --- apps/api/openapi/main.jsonnet | 6 +- apps/api/openapi/openapi.json | 363 ++++++++++++++++++ .../openapi/paths/secret-providers.jsonnet | 54 +++ .../openapi/schemas/secret-providers.jsonnet | 80 ++++ apps/api/package.json | 1 + apps/api/src/routes/v1/workspaces/index.ts | 4 +- .../routes/v1/workspaces/secret-providers.ts | 172 +++++++++ apps/api/src/types/openapi.ts | 265 +++++++++++++ e2e/api/schema.ts | 265 +++++++++++++ e2e/tests/api/secret-providers.spec.ts | 190 +++++++++ pnpm-lock.yaml | 59 ++- 11 files changed, 1452 insertions(+), 7 deletions(-) create mode 100644 apps/api/openapi/paths/secret-providers.jsonnet create mode 100644 apps/api/openapi/schemas/secret-providers.jsonnet create mode 100644 apps/api/src/routes/v1/workspaces/secret-providers.ts create mode 100644 e2e/tests/api/secret-providers.spec.ts diff --git a/apps/api/openapi/main.jsonnet b/apps/api/openapi/main.jsonnet index d8ba69eef..2f568a72d 100644 --- a/apps/api/openapi/main.jsonnet +++ b/apps/api/openapi/main.jsonnet @@ -50,7 +50,8 @@ local securitySchemes = { (import 'paths/release.jsonnet') + (import 'paths/job-agents.jsonnet') + (import 'paths/workflows.jsonnet') + - (import 'paths/variablesets.jsonnet'), + (import 'paths/variablesets.jsonnet') + + (import 'paths/secret-providers.jsonnet'), components: { parameters: {}, securitySchemes: securitySchemes, @@ -73,6 +74,7 @@ local securitySchemes = { (import 'schemas/job-agents.jsonnet') + (import 'schemas/verifications.jsonnet') + (import 'schemas/workflows.jsonnet') + - (import 'schemas/variablesets.jsonnet'), + (import 'schemas/variablesets.jsonnet') + + (import 'schemas/secret-providers.jsonnet'), }, } diff --git a/apps/api/openapi/openapi.json b/apps/api/openapi/openapi.json index 16bbfc399..49604bad3 100644 --- a/apps/api/openapi/openapi.json +++ b/apps/api/openapi/openapi.json @@ -21,6 +21,26 @@ ], "type": "string" }, + "AwsSecretsManagerConfig": { + "properties": { + "accessKeyId": { + "description": "Optional static AWS access key id. Omit to use the workspace-engine instance role.", + "type": "string" + }, + "region": { + "description": "AWS region.", + "type": "string" + }, + "secretAccessKey": { + "description": "Optional static AWS secret access key.", + "type": "string" + } + }, + "required": [ + "region" + ], + "type": "object" + }, "BooleanValue": { "type": "boolean" }, @@ -1052,6 +1072,34 @@ ], "type": "object" }, + "DopplerConfig": { + "properties": { + "serviceToken": { + "description": "Doppler service token (dp.st.<...>).", + "type": "string" + } + }, + "required": [ + "serviceToken" + ], + "type": "object" + }, + "EnvConfig": { + "properties": { + "allowedKeys": { + "description": "Explicit allowlist of environment variable names this provider may expose.", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "allowedKeys" + ], + "type": "object" + }, "Environment": { "properties": { "createdAt": { @@ -2430,6 +2478,80 @@ ], "type": "object" }, + "SecretProvider": { + "description": "Secret provider metadata. The encrypted configuration is never returned.", + "properties": { + "createdAt": { + "format": "date-time", + "type": "string" + }, + "id": { + "format": "uuid", + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/SecretProviderType" + }, + "updatedAt": { + "format": "date-time", + "type": "string" + }, + "workspaceId": { + "format": "uuid", + "type": "string" + } + }, + "required": [ + "id", + "workspaceId", + "name", + "type", + "createdAt", + "updatedAt" + ], + "type": "object" + }, + "SecretProviderConfig": { + "description": "Provider-specific configuration. Shape depends on the provider type.", + "oneOf": [ + { + "$ref": "#/components/schemas/AwsSecretsManagerConfig" + }, + { + "$ref": "#/components/schemas/DopplerConfig" + }, + { + "$ref": "#/components/schemas/EnvConfig" + } + ] + }, + "SecretProviderRequestAccepted": { + "properties": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "id", + "message" + ], + "type": "object" + }, + "SecretProviderType": { + "description": "Type of secret provider.", + "enum": [ + "aws_secrets_manager", + "doppler", + "env" + ], + "type": "string" + }, "SensitiveValue": { "properties": { "valueHash": { @@ -3055,6 +3177,26 @@ ], "type": "object" }, + "UpsertSecretProviderRequest": { + "properties": { + "config": { + "$ref": "#/components/schemas/SecretProviderConfig" + }, + "name": { + "description": "Workspace-unique name used to reference the provider from variable values.", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/SecretProviderType" + } + }, + "required": [ + "name", + "type", + "config" + ], + "type": "object" + }, "UpsertSystemRequest": { "properties": { "description": { @@ -8703,6 +8845,227 @@ ] } }, + "/v1/workspaces/{workspaceId}/secret-providers": { + "get": { + "description": "Returns the metadata of every secret provider configured in the workspace. Encrypted configurations are never returned.", + "operationId": "listSecretProviders", + "parameters": [ + { + "description": "ID of the workspace", + "in": "path", + "name": "workspaceId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Maximum number of items to return", + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 50, + "maximum": 1000, + "minimum": 1, + "type": "integer" + } + }, + { + "description": "Number of items to skip", + "in": "query", + "name": "offset", + "required": false, + "schema": { + "default": 0, + "minimum": 0, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/SecretProvider" + }, + "type": "array" + }, + "limit": { + "description": "Maximum number of items returned", + "type": "integer" + }, + "offset": { + "description": "Number of items skipped", + "type": "integer" + }, + "total": { + "description": "Total number of items available", + "type": "integer" + } + }, + "required": [ + "items", + "total", + "limit", + "offset" + ], + "type": "object" + } + } + }, + "description": "Paginated list of items" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + } + }, + "summary": "List secret providers" + } + }, + "/v1/workspaces/{workspaceId}/secret-providers/{providerId}": { + "delete": { + "description": "Variable values that reference this provider will fail to resolve until they are updated or the provider is recreated.", + "operationId": "requestSecretProviderDeletion", + "responses": { + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretProviderRequestAccepted" + } + } + }, + "description": "Secret provider deleted" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found" + } + }, + "summary": "Delete a secret provider" + }, + "get": { + "operationId": "getSecretProvider", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretProvider" + } + } + }, + "description": "OK response" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found" + } + }, + "summary": "Get a secret provider" + }, + "parameters": [ + { + "description": "ID of the workspace", + "in": "path", + "name": "workspaceId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "ID of the secret provider", + "in": "path", + "name": "providerId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "put": { + "description": "Creates or updates a secret provider. The config is encrypted at rest before persistence.", + "operationId": "requestSecretProviderUpsert", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpsertSecretProviderRequest" + } + } + }, + "required": true + }, + "responses": { + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecretProviderRequestAccepted" + } + } + }, + "description": "Accepted response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid request" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Resource not found" + } + }, + "summary": "Upsert a secret provider" + } + }, "/v1/workspaces/{workspaceId}/systems": { "get": { "operationId": "listSystems", diff --git a/apps/api/openapi/paths/secret-providers.jsonnet b/apps/api/openapi/paths/secret-providers.jsonnet new file mode 100644 index 000000000..9ed448e45 --- /dev/null +++ b/apps/api/openapi/paths/secret-providers.jsonnet @@ -0,0 +1,54 @@ +local openapi = import '../lib/openapi.libsonnet'; + +{ + '/v1/workspaces/{workspaceId}/secret-providers': { + get: { + summary: 'List secret providers', + operationId: 'listSecretProviders', + description: 'Returns the metadata of every secret provider configured in the workspace. Encrypted configurations are never returned.', + parameters: [ + openapi.workspaceIdParam(), + openapi.limitParam(), + openapi.offsetParam(), + ], + responses: openapi.paginatedResponse(openapi.schemaRef('SecretProvider')) + + openapi.badRequestResponse(), + }, + }, + '/v1/workspaces/{workspaceId}/secret-providers/{providerId}': { + parameters: [ + openapi.workspaceIdParam(), + openapi.stringParam('providerId', 'ID of the secret provider'), + ], + get: { + summary: 'Get a secret provider', + operationId: 'getSecretProvider', + responses: openapi.okResponse(openapi.schemaRef('SecretProvider')) + + openapi.notFoundResponse(), + }, + put: { + summary: 'Upsert a secret provider', + operationId: 'requestSecretProviderUpsert', + description: 'Creates or updates a secret provider. The config is encrypted at rest before persistence.', + requestBody: { + required: true, + content: { + 'application/json': { + schema: openapi.schemaRef('UpsertSecretProviderRequest'), + }, + }, + }, + responses: openapi.acceptedResponse(openapi.schemaRef('SecretProviderRequestAccepted')) + + openapi.notFoundResponse() + + openapi.badRequestResponse(), + }, + delete: { + summary: 'Delete a secret provider', + operationId: 'requestSecretProviderDeletion', + description: 'Variable values that reference this provider will fail to resolve until they are updated or the provider is recreated.', + responses: openapi.acceptedResponse(openapi.schemaRef('SecretProviderRequestAccepted'), 'Secret provider deleted') + + openapi.notFoundResponse() + + openapi.badRequestResponse(), + }, + }, +} diff --git a/apps/api/openapi/schemas/secret-providers.jsonnet b/apps/api/openapi/schemas/secret-providers.jsonnet new file mode 100644 index 000000000..e73eaa527 --- /dev/null +++ b/apps/api/openapi/schemas/secret-providers.jsonnet @@ -0,0 +1,80 @@ +{ + SecretProviderType: { + type: 'string', + enum: ['aws_secrets_manager', 'doppler', 'env'], + description: 'Type of secret provider.', + }, + + AwsSecretsManagerConfig: { + type: 'object', + required: ['region'], + properties: { + region: { type: 'string', description: 'AWS region.' }, + accessKeyId: { type: 'string', description: 'Optional static AWS access key id. Omit to use the workspace-engine instance role.' }, + secretAccessKey: { type: 'string', description: 'Optional static AWS secret access key.' }, + }, + }, + + DopplerConfig: { + type: 'object', + required: ['serviceToken'], + properties: { + serviceToken: { type: 'string', description: 'Doppler service token (dp.st.<...>).' }, + }, + }, + + EnvConfig: { + type: 'object', + required: ['allowedKeys'], + properties: { + allowedKeys: { + type: 'array', + items: { type: 'string' }, + minItems: 1, + description: 'Explicit allowlist of environment variable names this provider may expose.', + }, + }, + }, + + SecretProviderConfig: { + oneOf: [ + { '$ref': '#/components/schemas/AwsSecretsManagerConfig' }, + { '$ref': '#/components/schemas/DopplerConfig' }, + { '$ref': '#/components/schemas/EnvConfig' }, + ], + description: 'Provider-specific configuration. Shape depends on the provider type.', + }, + + UpsertSecretProviderRequest: { + type: 'object', + required: ['name', 'type', 'config'], + properties: { + name: { type: 'string', description: 'Workspace-unique name used to reference the provider from variable values.' }, + type: { '$ref': '#/components/schemas/SecretProviderType' }, + config: { '$ref': '#/components/schemas/SecretProviderConfig' }, + }, + }, + + SecretProvider: { + type: 'object', + required: ['id', 'workspaceId', 'name', 'type', 'createdAt', 'updatedAt'], + properties: { + id: { type: 'string', format: 'uuid' }, + workspaceId: { type: 'string', format: 'uuid' }, + name: { type: 'string' }, + type: { '$ref': '#/components/schemas/SecretProviderType' }, + createdAt: { type: 'string', format: 'date-time' }, + updatedAt: { type: 'string', format: 'date-time' }, + }, + description: 'Secret provider metadata. The encrypted configuration is never returned.', + }, + + SecretProviderRequestAccepted: { + type: 'object', + required: ['id', 'message'], + properties: { + id: { type: 'string' }, + message: { type: 'string' }, + }, + }, +} diff --git a/apps/api/package.json b/apps/api/package.json index f5728b677..dd9474464 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -26,6 +26,7 @@ "@ctrlplane/auth": "workspace:*", "@ctrlplane/db": "workspace:*", "@ctrlplane/logger": "workspace:*", + "@ctrlplane/secrets": "workspace:*", "@ctrlplane/trpc": "workspace:*", "@ctrlplane/validators": "workspace:*", "@ctrlplane/workspace-engine-sdk": "workspace:*", diff --git a/apps/api/src/routes/v1/workspaces/index.ts b/apps/api/src/routes/v1/workspaces/index.ts index 440d6889a..45989a09a 100644 --- a/apps/api/src/routes/v1/workspaces/index.ts +++ b/apps/api/src/routes/v1/workspaces/index.ts @@ -24,6 +24,7 @@ import { releaseTargetsRouter } from "./release-targets.js"; import { releaseRouter } from "./releases.js"; import { resourceProvidersRouter } from "./resource-providers.js"; import { resourceRouter } from "./resources.js"; +import { secretProvidersRouter } from "./secret-providers.js"; import { systemRouter } from "./systems.js"; import { variableSetsRouter } from "./variable-sets.js"; import { workflowsRouter } from "./workflows.js"; @@ -57,4 +58,5 @@ export const createWorkspacesRouter = (): Router => .use("/:workspaceId/releases", releaseRouter) .use("/:workspaceId/job-agents", jobAgentsRouter) .use("/:workspaceId/workflows", workflowsRouter) - .use("/:workspaceId/variable-sets", variableSetsRouter); + .use("/:workspaceId/variable-sets", variableSetsRouter) + .use("/:workspaceId/secret-providers", secretProvidersRouter); diff --git a/apps/api/src/routes/v1/workspaces/secret-providers.ts b/apps/api/src/routes/v1/workspaces/secret-providers.ts new file mode 100644 index 000000000..5ffeaa359 --- /dev/null +++ b/apps/api/src/routes/v1/workspaces/secret-providers.ts @@ -0,0 +1,172 @@ +import type { AsyncTypedHandler } from "@/types/api.js"; +import { ApiError, asyncHandler } from "@/types/api.js"; +import { Router } from "express"; +import { z } from "zod"; + +import { and, count, eq } from "@ctrlplane/db"; +import { db } from "@ctrlplane/db/client"; +import * as schema from "@ctrlplane/db/schema"; +import { variablesAES256 } from "@ctrlplane/secrets"; + +const awsSecretsManagerConfig = z.object({ + region: z.string().min(1), + accessKeyId: z.string().min(1).optional(), + secretAccessKey: z.string().min(1).optional(), +}); + +const dopplerConfig = z.object({ + serviceToken: z.string().startsWith("dp.st."), +}); + +const envConfig = z.object({ + allowedKeys: z + .array(z.string().regex(/^[A-Z_][A-Z0-9_]*$/)) + .min(1), +}); + +const providerBody = z.discriminatedUnion("type", [ + z.object({ + name: z.string().min(1), + type: z.literal("aws_secrets_manager"), + config: awsSecretsManagerConfig, + }), + z.object({ + name: z.string().min(1), + type: z.literal("doppler"), + config: dopplerConfig, + }), + z.object({ + name: z.string().min(1), + type: z.literal("env"), + config: envConfig, + }), +]); + +const formatSecretProvider = ( + row: typeof schema.secretProvider.$inferSelect, +) => ({ + id: row.id, + workspaceId: row.workspaceId, + name: row.name, + type: row.type, + createdAt: row.createdAt.toISOString(), + updatedAt: row.updatedAt.toISOString(), +}); + +const listSecretProviders: AsyncTypedHandler< + "/v1/workspaces/{workspaceId}/secret-providers", + "get" +> = async (req, res) => { + const { workspaceId } = req.params; + const limit = req.query.limit ?? 50; + const offset = req.query.offset ?? 0; + + const [countResult] = await db + .select({ total: count() }) + .from(schema.secretProvider) + .where(eq(schema.secretProvider.workspaceId, workspaceId)); + + const total = countResult?.total ?? 0; + + const items = await db + .select() + .from(schema.secretProvider) + .where(eq(schema.secretProvider.workspaceId, workspaceId)) + .limit(limit) + .offset(offset); + + res + .status(200) + .json({ items: items.map(formatSecretProvider), total, limit, offset }); +}; + +const getSecretProvider: AsyncTypedHandler< + "/v1/workspaces/{workspaceId}/secret-providers/{providerId}", + "get" +> = async (req, res) => { + const { workspaceId, providerId } = req.params; + + const row = await db.query.secretProvider.findFirst({ + where: and( + eq(schema.secretProvider.id, providerId), + eq(schema.secretProvider.workspaceId, workspaceId), + ), + }); + + if (row == null) throw new ApiError("Secret provider not found", 404); + + res.status(200).json(formatSecretProvider(row)); +}; + +const upsertSecretProvider: AsyncTypedHandler< + "/v1/workspaces/{workspaceId}/secret-providers/{providerId}", + "put" +> = async (req, res) => { + const { workspaceId, providerId } = req.params; + + const parsed = providerBody.safeParse(req.body); + if (!parsed.success) + throw new ApiError( + `Invalid secret provider body: ${parsed.error.message}`, + 400, + ); + + const { name, type, config } = parsed.data; + const encryptedConfig = Buffer.from( + variablesAES256().encrypt(JSON.stringify(config)), + "utf8", + ); + + await db + .insert(schema.secretProvider) + .values({ + id: providerId, + workspaceId, + name, + type, + config: encryptedConfig, + }) + .onConflictDoUpdate({ + target: schema.secretProvider.id, + set: { + name, + type, + config: encryptedConfig, + updatedAt: new Date(), + }, + }); + + res.status(202).json({ + id: providerId, + message: "Secret provider upsert requested", + }); +}; + +const deleteSecretProvider: AsyncTypedHandler< + "/v1/workspaces/{workspaceId}/secret-providers/{providerId}", + "delete" +> = async (req, res) => { + const { workspaceId, providerId } = req.params; + + const [deleted] = await db + .delete(schema.secretProvider) + .where( + and( + eq(schema.secretProvider.id, providerId), + eq(schema.secretProvider.workspaceId, workspaceId), + ), + ) + .returning(); + + if (deleted == null) throw new ApiError("Secret provider not found", 404); + + res + .status(202) + .json({ id: providerId, message: "Secret provider deleted" }); +}; + +export const secretProvidersRouter = Router({ mergeParams: true }) + .get("/", asyncHandler(listSecretProviders)) + .get("/:providerId", asyncHandler(getSecretProvider)) + .put("/:providerId", asyncHandler(upsertSecretProvider)) + .delete("/:providerId", asyncHandler(deleteSecretProvider)); diff --git a/apps/api/src/types/openapi.ts b/apps/api/src/types/openapi.ts index 4c7aefb22..98b6e0e5c 100644 --- a/apps/api/src/types/openapi.ts +++ b/apps/api/src/types/openapi.ts @@ -916,6 +916,56 @@ export interface paths { patch?: never; trace?: never; }; + "/v1/workspaces/{workspaceId}/secret-providers": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List secret providers + * @description Returns the metadata of every secret provider configured in the workspace. Encrypted configurations are never returned. + */ + get: operations["listSecretProviders"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/workspaces/{workspaceId}/secret-providers/{providerId}": { + parameters: { + query?: never; + header?: never; + path: { + /** @description ID of the workspace */ + workspaceId: string; + /** @description ID of the secret provider */ + providerId: string; + }; + cookie?: never; + }; + /** Get a secret provider */ + get: operations["getSecretProvider"]; + /** + * Upsert a secret provider + * @description Creates or updates a secret provider. The config is encrypted at rest before persistence. + */ + put: operations["requestSecretProviderUpsert"]; + post?: never; + /** + * Delete a secret provider + * @description Variable values that reference this provider will fail to resolve until they are updated or the provider is recreated. + */ + delete: operations["requestSecretProviderDeletion"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/v1/workspaces/{workspaceId}/systems": { parameters: { query?: never; @@ -1125,6 +1175,14 @@ export interface components { }; /** @enum {string} */ ApprovalStatus: "approved" | "rejected"; + AwsSecretsManagerConfig: { + /** @description Optional static AWS access key id. Omit to use the workspace-engine instance role. */ + accessKeyId?: string; + /** @description AWS region. */ + region: string; + /** @description Optional static AWS secret access key. */ + secretAccessKey?: string; + }; BooleanValue: boolean; CreateDeploymentPlanRequest: { /** @description Arbitrary key-value metadata for the plan (e.g. GitHub PR links, CI run URLs) */ @@ -1493,6 +1551,14 @@ export interface components { workflowJob?: components["schemas"]["WorkflowJob"]; workflowRun?: components["schemas"]["WorkflowRun"]; }; + DopplerConfig: { + /** @description Doppler service token (dp.st.<...>). */ + serviceToken: string; + }; + EnvConfig: { + /** @description Explicit allowlist of environment variable names this provider may expose. */ + allowedKeys: string[]; + }; Environment: { /** Format: date-time */ createdAt: string; @@ -1978,6 +2044,30 @@ export interface components { /** @description Job statuses that count toward the retry limit. If null or empty, defaults to ["failure", "invalidIntegration", "invalidJobAgent"] for maxRetries > 0, or ["failure", "invalidIntegration", "invalidJobAgent", "successful"] for maxRetries = 0. Cancelled and skipped jobs never count by default (allows redeployment after cancellation). Example: ["failure", "cancelled"] will only count failed/cancelled jobs. */ retryOnStatuses?: components["schemas"]["JobStatus"][]; }; + /** @description Secret provider metadata. The encrypted configuration is never returned. */ + SecretProvider: { + /** Format: date-time */ + createdAt: string; + /** Format: uuid */ + id: string; + name: string; + type: components["schemas"]["SecretProviderType"]; + /** Format: date-time */ + updatedAt: string; + /** Format: uuid */ + workspaceId: string; + }; + /** @description Provider-specific configuration. Shape depends on the provider type. */ + SecretProviderConfig: components["schemas"]["AwsSecretsManagerConfig"] | components["schemas"]["DopplerConfig"] | components["schemas"]["EnvConfig"]; + SecretProviderRequestAccepted: { + id: string; + message: string; + }; + /** + * @description Type of secret provider. + * @enum {string} + */ + SecretProviderType: "aws_secrets_manager" | "doppler" | "env"; SensitiveValue: { valueHash: string; }; @@ -2203,6 +2293,12 @@ export interface components { }; version: string; }; + UpsertSecretProviderRequest: { + config: components["schemas"]["SecretProviderConfig"]; + /** @description Workspace-unique name used to reference the provider from variable values. */ + name: string; + type: components["schemas"]["SecretProviderType"]; + }; UpsertSystemRequest: { description?: string; metadata?: { @@ -5682,6 +5778,175 @@ export interface operations { }; }; }; + listSecretProviders: { + parameters: { + query?: { + /** @description Maximum number of items to return */ + limit?: number; + /** @description Number of items to skip */ + offset?: number; + }; + header?: never; + path: { + /** @description ID of the workspace */ + workspaceId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Paginated list of items */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + items: components["schemas"]["SecretProvider"][]; + /** @description Maximum number of items returned */ + limit: number; + /** @description Number of items skipped */ + offset: number; + /** @description Total number of items available */ + total: number; + }; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + getSecretProvider: { + parameters: { + query?: never; + header?: never; + path: { + /** @description ID of the workspace */ + workspaceId: string; + /** @description ID of the secret provider */ + providerId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SecretProvider"]; + }; + }; + /** @description Resource not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + requestSecretProviderUpsert: { + parameters: { + query?: never; + header?: never; + path: { + /** @description ID of the workspace */ + workspaceId: string; + /** @description ID of the secret provider */ + providerId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpsertSecretProviderRequest"]; + }; + }; + responses: { + /** @description Accepted response */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SecretProviderRequestAccepted"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Resource not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + requestSecretProviderDeletion: { + parameters: { + query?: never; + header?: never; + path: { + /** @description ID of the workspace */ + workspaceId: string; + /** @description ID of the secret provider */ + providerId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Secret provider deleted */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SecretProviderRequestAccepted"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Resource not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; listSystems: { parameters: { query?: { diff --git a/e2e/api/schema.ts b/e2e/api/schema.ts index 4c7aefb22..98b6e0e5c 100644 --- a/e2e/api/schema.ts +++ b/e2e/api/schema.ts @@ -916,6 +916,56 @@ export interface paths { patch?: never; trace?: never; }; + "/v1/workspaces/{workspaceId}/secret-providers": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List secret providers + * @description Returns the metadata of every secret provider configured in the workspace. Encrypted configurations are never returned. + */ + get: operations["listSecretProviders"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/workspaces/{workspaceId}/secret-providers/{providerId}": { + parameters: { + query?: never; + header?: never; + path: { + /** @description ID of the workspace */ + workspaceId: string; + /** @description ID of the secret provider */ + providerId: string; + }; + cookie?: never; + }; + /** Get a secret provider */ + get: operations["getSecretProvider"]; + /** + * Upsert a secret provider + * @description Creates or updates a secret provider. The config is encrypted at rest before persistence. + */ + put: operations["requestSecretProviderUpsert"]; + post?: never; + /** + * Delete a secret provider + * @description Variable values that reference this provider will fail to resolve until they are updated or the provider is recreated. + */ + delete: operations["requestSecretProviderDeletion"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/v1/workspaces/{workspaceId}/systems": { parameters: { query?: never; @@ -1125,6 +1175,14 @@ export interface components { }; /** @enum {string} */ ApprovalStatus: "approved" | "rejected"; + AwsSecretsManagerConfig: { + /** @description Optional static AWS access key id. Omit to use the workspace-engine instance role. */ + accessKeyId?: string; + /** @description AWS region. */ + region: string; + /** @description Optional static AWS secret access key. */ + secretAccessKey?: string; + }; BooleanValue: boolean; CreateDeploymentPlanRequest: { /** @description Arbitrary key-value metadata for the plan (e.g. GitHub PR links, CI run URLs) */ @@ -1493,6 +1551,14 @@ export interface components { workflowJob?: components["schemas"]["WorkflowJob"]; workflowRun?: components["schemas"]["WorkflowRun"]; }; + DopplerConfig: { + /** @description Doppler service token (dp.st.<...>). */ + serviceToken: string; + }; + EnvConfig: { + /** @description Explicit allowlist of environment variable names this provider may expose. */ + allowedKeys: string[]; + }; Environment: { /** Format: date-time */ createdAt: string; @@ -1978,6 +2044,30 @@ export interface components { /** @description Job statuses that count toward the retry limit. If null or empty, defaults to ["failure", "invalidIntegration", "invalidJobAgent"] for maxRetries > 0, or ["failure", "invalidIntegration", "invalidJobAgent", "successful"] for maxRetries = 0. Cancelled and skipped jobs never count by default (allows redeployment after cancellation). Example: ["failure", "cancelled"] will only count failed/cancelled jobs. */ retryOnStatuses?: components["schemas"]["JobStatus"][]; }; + /** @description Secret provider metadata. The encrypted configuration is never returned. */ + SecretProvider: { + /** Format: date-time */ + createdAt: string; + /** Format: uuid */ + id: string; + name: string; + type: components["schemas"]["SecretProviderType"]; + /** Format: date-time */ + updatedAt: string; + /** Format: uuid */ + workspaceId: string; + }; + /** @description Provider-specific configuration. Shape depends on the provider type. */ + SecretProviderConfig: components["schemas"]["AwsSecretsManagerConfig"] | components["schemas"]["DopplerConfig"] | components["schemas"]["EnvConfig"]; + SecretProviderRequestAccepted: { + id: string; + message: string; + }; + /** + * @description Type of secret provider. + * @enum {string} + */ + SecretProviderType: "aws_secrets_manager" | "doppler" | "env"; SensitiveValue: { valueHash: string; }; @@ -2203,6 +2293,12 @@ export interface components { }; version: string; }; + UpsertSecretProviderRequest: { + config: components["schemas"]["SecretProviderConfig"]; + /** @description Workspace-unique name used to reference the provider from variable values. */ + name: string; + type: components["schemas"]["SecretProviderType"]; + }; UpsertSystemRequest: { description?: string; metadata?: { @@ -5682,6 +5778,175 @@ export interface operations { }; }; }; + listSecretProviders: { + parameters: { + query?: { + /** @description Maximum number of items to return */ + limit?: number; + /** @description Number of items to skip */ + offset?: number; + }; + header?: never; + path: { + /** @description ID of the workspace */ + workspaceId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Paginated list of items */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + items: components["schemas"]["SecretProvider"][]; + /** @description Maximum number of items returned */ + limit: number; + /** @description Number of items skipped */ + offset: number; + /** @description Total number of items available */ + total: number; + }; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + getSecretProvider: { + parameters: { + query?: never; + header?: never; + path: { + /** @description ID of the workspace */ + workspaceId: string; + /** @description ID of the secret provider */ + providerId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SecretProvider"]; + }; + }; + /** @description Resource not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + requestSecretProviderUpsert: { + parameters: { + query?: never; + header?: never; + path: { + /** @description ID of the workspace */ + workspaceId: string; + /** @description ID of the secret provider */ + providerId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpsertSecretProviderRequest"]; + }; + }; + responses: { + /** @description Accepted response */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SecretProviderRequestAccepted"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Resource not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + requestSecretProviderDeletion: { + parameters: { + query?: never; + header?: never; + path: { + /** @description ID of the workspace */ + workspaceId: string; + /** @description ID of the secret provider */ + providerId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Secret provider deleted */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SecretProviderRequestAccepted"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Resource not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; listSystems: { parameters: { query?: { diff --git a/e2e/tests/api/secret-providers.spec.ts b/e2e/tests/api/secret-providers.spec.ts new file mode 100644 index 000000000..5be659af1 --- /dev/null +++ b/e2e/tests/api/secret-providers.spec.ts @@ -0,0 +1,190 @@ +import { faker } from "@faker-js/faker"; +import { expect } from "@playwright/test"; +import { v4 as uuidv4 } from "uuid"; + +import { test } from "../fixtures"; + +test.describe("Secret Provider API", () => { + test("upserts, retrieves, lists, and deletes a provider", async ({ + api, + workspace, + }) => { + const providerId = uuidv4(); + const name = `sp-${faker.string.alphanumeric(8)}`; + + const upsertRes = await api.PUT( + "/v1/workspaces/{workspaceId}/secret-providers/{providerId}", + { + params: { path: { workspaceId: workspace.id, providerId } }, + body: { + name, + type: "doppler", + config: { serviceToken: "dp.st.testtoken1234567890" }, + }, + }, + ); + + try { + expect(upsertRes.response.status).toBe(202); + expect(upsertRes.data!.id).toBe(providerId); + + const getRes = await api.GET( + "/v1/workspaces/{workspaceId}/secret-providers/{providerId}", + { + params: { path: { workspaceId: workspace.id, providerId } }, + }, + ); + + expect(getRes.response.status).toBe(200); + expect(getRes.data!.id).toBe(providerId); + expect(getRes.data!.name).toBe(name); + expect(getRes.data!.type).toBe("doppler"); + expect(getRes.data!.workspaceId).toBe(workspace.id); + // Encrypted config must never be returned. + expect((getRes.data as Record).config).toBeUndefined(); + + const listRes = await api.GET( + "/v1/workspaces/{workspaceId}/secret-providers", + { + params: { path: { workspaceId: workspace.id } }, + }, + ); + + expect(listRes.response.status).toBe(200); + expect(listRes.data!.items.some((p) => p.id === providerId)).toBe(true); + expect( + listRes.data!.items.every( + (p) => (p as Record).config === undefined, + ), + ).toBe(true); + } finally { + await api.DELETE( + "/v1/workspaces/{workspaceId}/secret-providers/{providerId}", + { + params: { path: { workspaceId: workspace.id, providerId } }, + }, + ); + } + }); + + test("rejects unknown providerId on get", async ({ api, workspace }) => { + const getRes = await api.GET( + "/v1/workspaces/{workspaceId}/secret-providers/{providerId}", + { + params: { path: { workspaceId: workspace.id, providerId: uuidv4() } }, + }, + ); + + expect(getRes.response.status).toBe(404); + }); + + test("rejects delete on unknown providerId", async ({ api, workspace }) => { + const deleteRes = await api.DELETE( + "/v1/workspaces/{workspaceId}/secret-providers/{providerId}", + { + params: { path: { workspaceId: workspace.id, providerId: uuidv4() } }, + }, + ); + + expect(deleteRes.response.status).toBe(404); + }); + + test("accepts repeat upsert (idempotent on same id)", async ({ + api, + workspace, + }) => { + const providerId = uuidv4(); + const name = `sp-idem-${faker.string.alphanumeric(8)}`; + + const first = await api.PUT( + "/v1/workspaces/{workspaceId}/secret-providers/{providerId}", + { + params: { path: { workspaceId: workspace.id, providerId } }, + body: { + name, + type: "aws_secrets_manager", + config: { region: "us-east-1" }, + }, + }, + ); + + try { + expect(first.response.status).toBe(202); + + const second = await api.PUT( + "/v1/workspaces/{workspaceId}/secret-providers/{providerId}", + { + params: { path: { workspaceId: workspace.id, providerId } }, + body: { + name, + type: "aws_secrets_manager", + config: { region: "us-west-2" }, + }, + }, + ); + + expect(second.response.status).toBe(202); + + const getRes = await api.GET( + "/v1/workspaces/{workspaceId}/secret-providers/{providerId}", + { + params: { path: { workspaceId: workspace.id, providerId } }, + }, + ); + + expect(getRes.response.status).toBe(200); + expect(getRes.data!.name).toBe(name); + expect(getRes.data!.type).toBe("aws_secrets_manager"); + } finally { + await api.DELETE( + "/v1/workspaces/{workspaceId}/secret-providers/{providerId}", + { + params: { path: { workspaceId: workspace.id, providerId } }, + }, + ); + } + }); + + test("rejects env provider with empty allowlist", async ({ + api, + workspace, + }) => { + const providerId = uuidv4(); + const upsertRes = await api.PUT( + "/v1/workspaces/{workspaceId}/secret-providers/{providerId}", + { + params: { path: { workspaceId: workspace.id, providerId } }, + // Body fails OpenAPI minItems validation before reaching the handler. + body: { + name: `sp-bad-${faker.string.alphanumeric(8)}`, + type: "env", + // @ts-expect-error intentionally invalid for the test + config: { allowedKeys: [] }, + }, + }, + ); + + expect(upsertRes.response.status).toBe(400); + }); + + test("rejects doppler provider with malformed token", async ({ + api, + workspace, + }) => { + const providerId = uuidv4(); + const upsertRes = await api.PUT( + "/v1/workspaces/{workspaceId}/secret-providers/{providerId}", + { + params: { path: { workspaceId: workspace.id, providerId } }, + body: { + name: `sp-bad-${faker.string.alphanumeric(8)}`, + type: "doppler", + // Doesn't start with dp.st. - Zod discriminator rejects. + config: { serviceToken: "not-a-doppler-token" }, + }, + }, + ); + + expect(upsertRes.response.status).toBe(400); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index daaceb03b..c7d153736 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -132,6 +132,9 @@ importers: '@ctrlplane/logger': specifier: workspace:* version: link:../../packages/logger + '@ctrlplane/secrets': + specifier: workspace:* + version: link:../../packages/secrets '@ctrlplane/trpc': specifier: workspace:* version: link:../../packages/trpc @@ -206,7 +209,7 @@ importers: version: 2.4.3 better-auth: specifier: ^1.4.6 - version: 1.4.6(next@15.2.4(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.53.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + version: 1.4.6(next@15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.53.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) cel-js: specifier: ^0.8.2 version: 0.8.2 @@ -670,7 +673,7 @@ importers: version: 0.11.1(typescript@5.9.3)(zod@3.24.2) better-auth: specifier: ^1.4.6 - version: 1.4.6(next@15.2.4(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.53.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + version: 1.4.6(next@15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.53.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) lodash: specifier: 'catalog:' version: 4.17.21 @@ -957,7 +960,7 @@ importers: version: 11.0.0-rc.364 better-auth: specifier: ^1.4.6 - version: 1.4.6(next@15.2.4(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.53.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + version: 1.4.6(next@15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.53.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) cel-js: specifier: ^0.8.2 version: 0.8.2 @@ -5201,7 +5204,7 @@ packages: basic-ftp@5.0.5: resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} engines: {node: '>=10.0.0'} - deprecated: Security vulnerability fixed in 5.2.1, please upgrade + deprecated: Security vulnerability fixed in 5.2.0, please upgrade bcryptjs@2.4.3: resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==} @@ -13628,6 +13631,26 @@ snapshots: react: 19.2.1 react-dom: 19.2.1(react@19.2.1) + better-auth@1.4.6(next@15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.53.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + dependencies: + '@better-auth/core': 1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@3.24.2))(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1) + '@better-auth/telemetry': 1.4.6(@better-auth/core@1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@3.24.2))(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1)) + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.18 + '@noble/ciphers': 2.0.1 + '@noble/hashes': 2.0.1 + better-call: 1.1.5(zod@4.1.12) + defu: 6.1.4 + jose: 6.1.0 + kysely: 0.28.8 + ms: 4.0.0-nightly.202508271359 + nanostores: 1.0.1 + zod: 4.1.12 + optionalDependencies: + next: 15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.53.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + better-call@1.1.5(zod@4.1.12): dependencies: '@better-auth/utils': 0.3.0 @@ -16377,6 +16400,34 @@ snapshots: - babel-plugin-macros optional: true + next@15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.53.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + dependencies: + '@next/env': 15.2.4 + '@swc/counter': 0.1.3 + '@swc/helpers': 0.5.15 + busboy: 1.6.0 + caniuse-lite: 1.0.30001760 + postcss: 8.4.31 + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + styled-jsx: 5.1.6(@babel/core@7.24.5)(react@19.2.1) + optionalDependencies: + '@next/swc-darwin-arm64': 15.2.4 + '@next/swc-darwin-x64': 15.2.4 + '@next/swc-linux-arm64-gnu': 15.2.4 + '@next/swc-linux-arm64-musl': 15.2.4 + '@next/swc-linux-x64-gnu': 15.2.4 + '@next/swc-linux-x64-musl': 15.2.4 + '@next/swc-win32-arm64-msvc': 15.2.4 + '@next/swc-win32-x64-msvc': 15.2.4 + '@opentelemetry/api': 1.9.0 + '@playwright/test': 1.53.2 + sharp: 0.33.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + optional: true + no-case@2.3.2: dependencies: lower-case: 1.1.4 From 009bf206af0d5c64c0327b45a93ab6c4f296c1eb Mon Sep 17 00:00:00 2001 From: Michael Leone Date: Mon, 11 May 2026 12:21:56 -0400 Subject: [PATCH 03/13] feat(secrets): add Go core for secret provider resolution - pkg/db: secret_provider added to the embedded sqlc schema; new secret_providers.sql with GetSecretProviderByName + ListSecretProvidersByWorkspaceID queries; sqlc regenerated. - pkg/secrets: package scaffold for resolving variable_value rows of kind = secret_ref. - types.go: SecretReference, Provider, ProviderConfig, ProviderFactory, ProviderConfigStore interfaces. - registry.go: type-keyed factory lookup, mirrors pkg/jobagents/registry.go. - store.go: sqlc-backed PostgresStore that loads secret_provider rows and decrypts the bytea config in-memory via a Decryptor (AES-256-CBC, interoperable with @ctrlplane/secrets). - cache.go: goroutine-safe TTL cache keyed by (workspaceID, providerName, path, key) with InvalidateProvider hook for LISTEN/NOTIFY consumers. - resolver.go: glues store -> registry -> provider with optional cache, rejects empty Provider/Key, propagates upstream errors. - Unit tests cover cache hit/miss/expiry/invalidation, registry build and factory errors, resolver happy path, cache reuse, post-invalidate refetch, validation, and store/provider error propagation. -race clean --- apps/workspace-engine/pkg/db/models.go | 53 ++++ .../pkg/db/queries/schema.sql | 15 +- .../pkg/db/queries/secret_providers.sql | 10 + .../pkg/db/secret_providers.sql.go | 73 ++++++ apps/workspace-engine/pkg/db/sqlc.yaml | 1 + apps/workspace-engine/pkg/secrets/cache.go | 112 ++++++++ .../pkg/secrets/cache_test.go | 102 ++++++++ apps/workspace-engine/pkg/secrets/registry.go | 41 +++ .../pkg/secrets/registry_test.go | 58 +++++ apps/workspace-engine/pkg/secrets/resolver.go | 77 ++++++ .../pkg/secrets/resolver_test.go | 241 ++++++++++++++++++ apps/workspace-engine/pkg/secrets/store.go | 100 ++++++++ apps/workspace-engine/pkg/secrets/types.go | 54 ++++ 13 files changed, 936 insertions(+), 1 deletion(-) create mode 100644 apps/workspace-engine/pkg/db/queries/secret_providers.sql create mode 100644 apps/workspace-engine/pkg/db/secret_providers.sql.go create mode 100644 apps/workspace-engine/pkg/secrets/cache.go create mode 100644 apps/workspace-engine/pkg/secrets/cache_test.go create mode 100644 apps/workspace-engine/pkg/secrets/registry.go create mode 100644 apps/workspace-engine/pkg/secrets/registry_test.go create mode 100644 apps/workspace-engine/pkg/secrets/resolver.go create mode 100644 apps/workspace-engine/pkg/secrets/resolver_test.go create mode 100644 apps/workspace-engine/pkg/secrets/store.go create mode 100644 apps/workspace-engine/pkg/secrets/types.go diff --git a/apps/workspace-engine/pkg/db/models.go b/apps/workspace-engine/pkg/db/models.go index d6d9144f7..36962dbcf 100644 --- a/apps/workspace-engine/pkg/db/models.go +++ b/apps/workspace-engine/pkg/db/models.go @@ -283,6 +283,49 @@ func (ns NullJobVerificationTriggerOn) Value() (driver.Value, error) { return string(ns.JobVerificationTriggerOn), nil } +type SecretProviderType string + +const ( + SecretProviderTypeAwsSecretsManager SecretProviderType = "aws_secrets_manager" + SecretProviderTypeDoppler SecretProviderType = "doppler" + SecretProviderTypeEnv SecretProviderType = "env" +) + +func (e *SecretProviderType) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = SecretProviderType(s) + case string: + *e = SecretProviderType(s) + default: + return fmt.Errorf("unsupported scan type for SecretProviderType: %T", src) + } + return nil +} + +type NullSecretProviderType struct { + SecretProviderType SecretProviderType + Valid bool // Valid is true if SecretProviderType is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullSecretProviderType) Scan(value interface{}) error { + if value == nil { + ns.SecretProviderType, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.SecretProviderType.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullSecretProviderType) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.SecretProviderType), nil +} + type VariableScope string const ( @@ -761,6 +804,16 @@ type ResourceProvider struct { Metadata map[string]string } +type SecretProvider struct { + ID uuid.UUID + WorkspaceID uuid.UUID + Name string + Type SecretProviderType + Config []byte + CreatedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz +} + type System struct { ID uuid.UUID Name string diff --git a/apps/workspace-engine/pkg/db/queries/schema.sql b/apps/workspace-engine/pkg/db/queries/schema.sql index bbfe1224b..756b1d6c4 100644 --- a/apps/workspace-engine/pkg/db/queries/schema.sql +++ b/apps/workspace-engine/pkg/db/queries/schema.sql @@ -546,4 +546,17 @@ CREATE TABLE deployment_plan_target_result_validation ( violations JSONB NOT NULL DEFAULT '[]'::jsonb, evaluated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (result_id, rule_id) -); \ No newline at end of file +); + +CREATE TYPE secret_provider_type AS ENUM ('aws_secrets_manager', 'doppler', 'env'); + +CREATE TABLE secret_provider ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + workspace_id UUID NOT NULL REFERENCES workspace(id) ON DELETE CASCADE, + name TEXT NOT NULL, + type secret_provider_type NOT NULL, + config BYTEA NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (workspace_id, name) +); diff --git a/apps/workspace-engine/pkg/db/queries/secret_providers.sql b/apps/workspace-engine/pkg/db/queries/secret_providers.sql new file mode 100644 index 000000000..801cdd7bd --- /dev/null +++ b/apps/workspace-engine/pkg/db/queries/secret_providers.sql @@ -0,0 +1,10 @@ +-- name: GetSecretProviderByName :one +SELECT id, workspace_id, name, type, config, created_at, updated_at +FROM secret_provider +WHERE workspace_id = $1 AND name = $2; + +-- name: ListSecretProvidersByWorkspaceID :many +SELECT id, workspace_id, name, type, config, created_at, updated_at +FROM secret_provider +WHERE workspace_id = $1 +ORDER BY name; diff --git a/apps/workspace-engine/pkg/db/secret_providers.sql.go b/apps/workspace-engine/pkg/db/secret_providers.sql.go new file mode 100644 index 000000000..b60927a39 --- /dev/null +++ b/apps/workspace-engine/pkg/db/secret_providers.sql.go @@ -0,0 +1,73 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: secret_providers.sql + +package db + +import ( + "context" + + "github.com/google/uuid" +) + +const getSecretProviderByName = `-- name: GetSecretProviderByName :one +SELECT id, workspace_id, name, type, config, created_at, updated_at +FROM secret_provider +WHERE workspace_id = $1 AND name = $2 +` + +type GetSecretProviderByNameParams struct { + WorkspaceID uuid.UUID + Name string +} + +func (q *Queries) GetSecretProviderByName(ctx context.Context, arg GetSecretProviderByNameParams) (SecretProvider, error) { + row := q.db.QueryRow(ctx, getSecretProviderByName, arg.WorkspaceID, arg.Name) + var i SecretProvider + err := row.Scan( + &i.ID, + &i.WorkspaceID, + &i.Name, + &i.Type, + &i.Config, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const listSecretProvidersByWorkspaceID = `-- name: ListSecretProvidersByWorkspaceID :many +SELECT id, workspace_id, name, type, config, created_at, updated_at +FROM secret_provider +WHERE workspace_id = $1 +ORDER BY name +` + +func (q *Queries) ListSecretProvidersByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]SecretProvider, error) { + rows, err := q.db.Query(ctx, listSecretProvidersByWorkspaceID, workspaceID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SecretProvider + for rows.Next() { + var i SecretProvider + if err := rows.Scan( + &i.ID, + &i.WorkspaceID, + &i.Name, + &i.Type, + &i.Config, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/apps/workspace-engine/pkg/db/sqlc.yaml b/apps/workspace-engine/pkg/db/sqlc.yaml index 56670b9fd..036f4aa7b 100644 --- a/apps/workspace-engine/pkg/db/sqlc.yaml +++ b/apps/workspace-engine/pkg/db/sqlc.yaml @@ -33,6 +33,7 @@ sql: - queries/release_targets.sql - queries/variable_sets.sql - queries/plan_validation.sql + - queries/secret_providers.sql database: uri: "postgresql://ctrlplane:ctrlplane@127.0.0.1:5432/ctrlplane?sslmode=disable" gen: diff --git a/apps/workspace-engine/pkg/secrets/cache.go b/apps/workspace-engine/pkg/secrets/cache.go new file mode 100644 index 000000000..d7e092fce --- /dev/null +++ b/apps/workspace-engine/pkg/secrets/cache.go @@ -0,0 +1,112 @@ +package secrets + +import ( + "sync" + "time" + + "github.com/google/uuid" +) + +// cacheKey identifies a single resolved secret value. Path is normalized to +// "" when absent so that ":::" and "::/:" +// don't collide. +type cacheKey struct { + WorkspaceID uuid.UUID + Provider string + Path string + Key string +} + +type cacheEntry struct { + value string + expiresAt time.Time +} + +// Cache is a goroutine-safe TTL cache for resolved secret values. Entries +// expire passively on read; explicit invalidation is provided for provider +// updates received over LISTEN/NOTIFY. +type Cache struct { + ttl time.Duration + now func() time.Time + mu sync.RWMutex + entries map[cacheKey]cacheEntry +} + +// NewCache constructs an empty cache. ttl of zero disables caching (every Get +// returns a miss). +func NewCache(ttl time.Duration) *Cache { + return &Cache{ + ttl: ttl, + now: time.Now, + entries: make(map[cacheKey]cacheEntry), + } +} + +func keyFor(workspaceID uuid.UUID, ref SecretReference) cacheKey { + return cacheKey{ + WorkspaceID: workspaceID, + Provider: ref.Provider, + Path: ref.Path, + Key: ref.Key, + } +} + +// Get returns the cached value if present and unexpired. The boolean return +// distinguishes a cache hit from a miss. +func (c *Cache) Get(workspaceID uuid.UUID, ref SecretReference) (string, bool) { + if c.ttl <= 0 { + return "", false + } + c.mu.RLock() + entry, ok := c.entries[keyFor(workspaceID, ref)] + c.mu.RUnlock() + if !ok { + return "", false + } + if c.now().After(entry.expiresAt) { + return "", false + } + return entry.value, true +} + +// Set stores a resolved value. The TTL is taken from the cache; per-entry +// TTLs are not supported. +func (c *Cache) Set(workspaceID uuid.UUID, ref SecretReference, value string) { + if c.ttl <= 0 { + return + } + c.mu.Lock() + c.entries[keyFor(workspaceID, ref)] = cacheEntry{ + value: value, + expiresAt: c.now().Add(c.ttl), + } + c.mu.Unlock() +} + +// InvalidateProvider drops every entry that resolves through the named +// provider in the given workspace. Called by the LISTEN/NOTIFY consumer when +// the TS api updates a secret_provider row. +func (c *Cache) InvalidateProvider(workspaceID uuid.UUID, providerName string) { + c.mu.Lock() + defer c.mu.Unlock() + for k := range c.entries { + if k.WorkspaceID == workspaceID && k.Provider == providerName { + delete(c.entries, k) + } + } +} + +// InvalidateAll empties the cache. Intended for tests and admin operations. +func (c *Cache) InvalidateAll() { + c.mu.Lock() + c.entries = make(map[cacheKey]cacheEntry) + c.mu.Unlock() +} + +// Size returns the number of entries currently cached. Expired entries that +// have not yet been observed by Get are counted. +func (c *Cache) Size() int { + c.mu.RLock() + defer c.mu.RUnlock() + return len(c.entries) +} diff --git a/apps/workspace-engine/pkg/secrets/cache_test.go b/apps/workspace-engine/pkg/secrets/cache_test.go new file mode 100644 index 000000000..6e9dba3b2 --- /dev/null +++ b/apps/workspace-engine/pkg/secrets/cache_test.go @@ -0,0 +1,102 @@ +package secrets + +import ( + "testing" + "time" + + "github.com/google/uuid" +) + +func TestCacheHitMiss(t *testing.T) { + c := NewCache(time.Minute) + ws := uuid.New() + ref := SecretReference{Provider: "doppler-prod", Path: "backend/prod", Key: "TOKEN"} + + if _, ok := c.Get(ws, ref); ok { + t.Fatal("expected miss on empty cache") + } + + c.Set(ws, ref, "secret-value") + + v, ok := c.Get(ws, ref) + if !ok || v != "secret-value" { + t.Fatalf("expected hit with value=%q, got ok=%v value=%q", "secret-value", ok, v) + } +} + +func TestCacheExpiry(t *testing.T) { + c := NewCache(time.Minute) + now := time.Unix(1_700_000_000, 0) + c.now = func() time.Time { return now } + + ws := uuid.New() + ref := SecretReference{Provider: "p", Key: "K"} + + c.Set(ws, ref, "v1") + + if _, ok := c.Get(ws, ref); !ok { + t.Fatal("expected hit immediately after Set") + } + + now = now.Add(time.Minute + time.Second) + if _, ok := c.Get(ws, ref); ok { + t.Fatal("expected miss after TTL expiry") + } +} + +func TestCacheDisabledWhenTTLZero(t *testing.T) { + c := NewCache(0) + ws := uuid.New() + ref := SecretReference{Provider: "p", Key: "K"} + + c.Set(ws, ref, "v1") + if _, ok := c.Get(ws, ref); ok { + t.Fatal("zero TTL must disable caching") + } + if c.Size() != 0 { + t.Fatalf("zero TTL must not store entries, got %d", c.Size()) + } +} + +func TestCacheInvalidateProvider(t *testing.T) { + c := NewCache(time.Minute) + wsA := uuid.New() + wsB := uuid.New() + + c.Set(wsA, SecretReference{Provider: "doppler-prod", Key: "K1"}, "a") + c.Set(wsA, SecretReference{Provider: "doppler-prod", Key: "K2"}, "b") + c.Set(wsA, SecretReference{Provider: "aws-prod", Key: "K3"}, "c") + c.Set(wsB, SecretReference{Provider: "doppler-prod", Key: "K1"}, "d") + + c.InvalidateProvider(wsA, "doppler-prod") + + if _, ok := c.Get(wsA, SecretReference{Provider: "doppler-prod", Key: "K1"}); ok { + t.Fatal("expected wsA/doppler-prod/K1 evicted") + } + if _, ok := c.Get(wsA, SecretReference{Provider: "doppler-prod", Key: "K2"}); ok { + t.Fatal("expected wsA/doppler-prod/K2 evicted") + } + if _, ok := c.Get(wsA, SecretReference{Provider: "aws-prod", Key: "K3"}); !ok { + t.Fatal("expected wsA/aws-prod/K3 retained") + } + if _, ok := c.Get(wsB, SecretReference{Provider: "doppler-prod", Key: "K1"}); !ok { + t.Fatal("expected wsB/doppler-prod/K1 retained (different workspace)") + } +} + +func TestCacheKeysDistinguishPaths(t *testing.T) { + c := NewCache(time.Minute) + ws := uuid.New() + + c.Set(ws, SecretReference{Provider: "p", Path: "a", Key: "K"}, "value-a") + c.Set(ws, SecretReference{Provider: "p", Path: "b", Key: "K"}, "value-b") + + v, _ := c.Get(ws, SecretReference{Provider: "p", Path: "a", Key: "K"}) + if v != "value-a" { + t.Fatalf("path a: want value-a, got %q", v) + } + v, _ = c.Get(ws, SecretReference{Provider: "p", Path: "b", Key: "K"}) + if v != "value-b" { + t.Fatalf("path b: want value-b, got %q", v) + } +} diff --git a/apps/workspace-engine/pkg/secrets/registry.go b/apps/workspace-engine/pkg/secrets/registry.go new file mode 100644 index 000000000..7b1b5faab --- /dev/null +++ b/apps/workspace-engine/pkg/secrets/registry.go @@ -0,0 +1,41 @@ +package secrets + +import "fmt" + +// Registry is a string-keyed lookup of ProviderFactory by secret_provider.type. +// Provider packages register themselves at init time, mirroring the +// jobagents/registry.go pattern. +type Registry struct { + factories map[string]ProviderFactory +} + +func NewRegistry() *Registry { + return &Registry{factories: make(map[string]ProviderFactory)} +} + +// Register attaches a factory under the given provider type. A second call +// with the same type overwrites the prior registration; callers should treat +// re-registration as a programming error and avoid it. +func (r *Registry) Register(providerType string, factory ProviderFactory) { + r.factories[providerType] = factory +} + +// Build constructs a Provider from a decrypted ProviderConfig. Returns an +// error if no factory is registered for the config's type. +func (r *Registry) Build(cfg *ProviderConfig) (Provider, error) { + factory, ok := r.factories[cfg.Type] + if !ok { + return nil, fmt.Errorf("secrets: no provider factory registered for type %q", cfg.Type) + } + return factory(cfg.Config) +} + +// Types returns the registered provider types in undefined order. Primarily +// useful for diagnostics and startup logging. +func (r *Registry) Types() []string { + types := make([]string, 0, len(r.factories)) + for t := range r.factories { + types = append(types, t) + } + return types +} diff --git a/apps/workspace-engine/pkg/secrets/registry_test.go b/apps/workspace-engine/pkg/secrets/registry_test.go new file mode 100644 index 000000000..b68e61dbc --- /dev/null +++ b/apps/workspace-engine/pkg/secrets/registry_test.go @@ -0,0 +1,58 @@ +package secrets + +import ( + "context" + "errors" + "testing" +) + +type stubProvider struct{ name string } + +func (s *stubProvider) Type() string { return s.name } + +func (s *stubProvider) Resolve(_ context.Context, _ SecretReference) (string, error) { + return "", errors.New("not implemented") +} + +func TestRegistryBuildAndTypes(t *testing.T) { + r := NewRegistry() + r.Register("doppler", func(_ map[string]any) (Provider, error) { + return &stubProvider{name: "doppler"}, nil + }) + r.Register("aws_secrets_manager", func(_ map[string]any) (Provider, error) { + return &stubProvider{name: "aws_secrets_manager"}, nil + }) + + types := r.Types() + if len(types) != 2 { + t.Fatalf("expected 2 registered types, got %d (%v)", len(types), types) + } + + p, err := r.Build(&ProviderConfig{Type: "doppler", Config: map[string]any{}}) + if err != nil { + t.Fatalf("Build doppler: %v", err) + } + if p.Type() != "doppler" { + t.Fatalf("got type %q, want doppler", p.Type()) + } +} + +func TestRegistryUnknownTypeFails(t *testing.T) { + r := NewRegistry() + _, err := r.Build(&ProviderConfig{Type: "vault", Config: map[string]any{}}) + if err == nil { + t.Fatal("expected error for unregistered type") + } +} + +func TestRegistryFactoryErrorPropagates(t *testing.T) { + r := NewRegistry() + wantErr := errors.New("bad config") + r.Register("doppler", func(_ map[string]any) (Provider, error) { + return nil, wantErr + }) + _, err := r.Build(&ProviderConfig{Type: "doppler", Config: map[string]any{}}) + if !errors.Is(err, wantErr) { + t.Fatalf("expected wrapped wantErr, got %v", err) + } +} diff --git a/apps/workspace-engine/pkg/secrets/resolver.go b/apps/workspace-engine/pkg/secrets/resolver.go new file mode 100644 index 000000000..6316cd53f --- /dev/null +++ b/apps/workspace-engine/pkg/secrets/resolver.go @@ -0,0 +1,77 @@ +package secrets + +import ( + "context" + "fmt" + + "github.com/google/uuid" +) + +// Resolver glues the ProviderConfigStore (lookup + decrypt), the Registry +// (factory dispatch), and the Cache (TTL'd resolved values). One Resolver is +// constructed at startup and shared by all reconciliation goroutines. +type Resolver struct { + store ProviderConfigStore + registry *Registry + cache *Cache +} + +// NewResolver builds a Resolver. A nil cache disables caching. +func NewResolver(store ProviderConfigStore, registry *Registry, cache *Cache) *Resolver { + return &Resolver{store: store, registry: registry, cache: cache} +} + +// Resolve fetches the secret value identified by ref. Lookups proceed: +// +// 1. cache (if configured) +// 2. ProviderConfigStore.Get to load + decrypt the provider config +// 3. Registry.Build to construct a Provider from the config +// 4. Provider.Resolve to hit the upstream secret store +// +// Any error in steps 2-4 propagates; release dispatch is expected to block. +func (r *Resolver) Resolve( + ctx context.Context, + workspaceID uuid.UUID, + ref SecretReference, +) (string, error) { + if ref.Provider == "" { + return "", fmt.Errorf("secrets: empty provider name in reference") + } + if ref.Key == "" { + return "", fmt.Errorf("secrets: empty key in reference") + } + + if r.cache != nil { + if v, ok := r.cache.Get(workspaceID, ref); ok { + return v, nil + } + } + + cfg, err := r.store.Get(ctx, workspaceID, ref.Provider) + if err != nil { + return "", err + } + + provider, err := r.registry.Build(cfg) + if err != nil { + return "", err + } + + value, err := provider.Resolve(ctx, ref) + if err != nil { + return "", fmt.Errorf("secrets: provider %q (%s) resolve: %w", cfg.Name, cfg.Type, err) + } + + if r.cache != nil { + r.cache.Set(workspaceID, ref, value) + } + return value, nil +} + +// InvalidateProvider drops cached entries for the named provider. Wire to the +// LISTEN/NOTIFY consumer on the `secret_provider_invalidate` channel. +func (r *Resolver) InvalidateProvider(workspaceID uuid.UUID, providerName string) { + if r.cache != nil { + r.cache.InvalidateProvider(workspaceID, providerName) + } +} diff --git a/apps/workspace-engine/pkg/secrets/resolver_test.go b/apps/workspace-engine/pkg/secrets/resolver_test.go new file mode 100644 index 000000000..492712aa4 --- /dev/null +++ b/apps/workspace-engine/pkg/secrets/resolver_test.go @@ -0,0 +1,241 @@ +package secrets + +import ( + "context" + "errors" + "sync/atomic" + "testing" + "time" + + "github.com/google/uuid" +) + +type mockStore struct { + configs map[string]*ProviderConfig + getErr error + getCalls atomic.Int32 + lastLookup string +} + +func (m *mockStore) Get( + _ context.Context, + _ uuid.UUID, + providerName string, +) (*ProviderConfig, error) { + m.getCalls.Add(1) + m.lastLookup = providerName + if m.getErr != nil { + return nil, m.getErr + } + cfg, ok := m.configs[providerName] + if !ok { + return nil, errors.New("not found") + } + return cfg, nil +} + +func (m *mockStore) List( + _ context.Context, + _ uuid.UUID, +) ([]*ProviderConfig, error) { + out := make([]*ProviderConfig, 0, len(m.configs)) + for _, cfg := range m.configs { + out = append(out, cfg) + } + return out, nil +} + +type mockProvider struct { + t string + resolveErr error + resolveVal string + resolveRefs []SecretReference +} + +func (p *mockProvider) Type() string { return p.t } + +func (p *mockProvider) Resolve(_ context.Context, ref SecretReference) (string, error) { + p.resolveRefs = append(p.resolveRefs, ref) + if p.resolveErr != nil { + return "", p.resolveErr + } + return p.resolveVal, nil +} + +func newResolver(t *testing.T, store ProviderConfigStore, provider Provider) *Resolver { + t.Helper() + reg := NewRegistry() + reg.Register(provider.Type(), func(_ map[string]any) (Provider, error) { return provider, nil }) + return NewResolver(store, reg, NewCache(time.Minute)) +} + +func TestResolverHappyPath(t *testing.T) { + ws := uuid.New() + store := &mockStore{ + configs: map[string]*ProviderConfig{ + "doppler-prod": { + WorkspaceID: ws, + Name: "doppler-prod", + Type: "doppler", + Config: map[string]any{"serviceToken": "dp.st.test"}, + }, + }, + } + provider := &mockProvider{t: "doppler", resolveVal: "abc123"} + r := newResolver(t, store, provider) + + got, err := r.Resolve(context.Background(), ws, SecretReference{ + Provider: "doppler-prod", + Path: "backend/prod", + Key: "TOKEN", + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if got != "abc123" { + t.Fatalf("expected abc123, got %q", got) + } + if len(provider.resolveRefs) != 1 { + t.Fatalf("provider.Resolve called %d times, want 1", len(provider.resolveRefs)) + } +} + +func TestResolverCacheHitsSkipStore(t *testing.T) { + ws := uuid.New() + store := &mockStore{ + configs: map[string]*ProviderConfig{ + "doppler-prod": { + WorkspaceID: ws, + Name: "doppler-prod", + Type: "doppler", + Config: map[string]any{}, + }, + }, + } + provider := &mockProvider{t: "doppler", resolveVal: "abc123"} + r := newResolver(t, store, provider) + ref := SecretReference{Provider: "doppler-prod", Key: "TOKEN"} + + if _, err := r.Resolve(context.Background(), ws, ref); err != nil { + t.Fatalf("first Resolve: %v", err) + } + if _, err := r.Resolve(context.Background(), ws, ref); err != nil { + t.Fatalf("second Resolve: %v", err) + } + + if got := store.getCalls.Load(); got != 1 { + t.Fatalf("store.Get called %d times, want 1 (second should be cache hit)", got) + } + if got := len(provider.resolveRefs); got != 1 { + t.Fatalf("provider.Resolve called %d times, want 1", got) + } +} + +func TestResolverInvalidationForcesRefetch(t *testing.T) { + ws := uuid.New() + store := &mockStore{ + configs: map[string]*ProviderConfig{ + "doppler-prod": { + WorkspaceID: ws, + Name: "doppler-prod", + Type: "doppler", + Config: map[string]any{}, + }, + }, + } + provider := &mockProvider{t: "doppler", resolveVal: "abc"} + r := newResolver(t, store, provider) + ref := SecretReference{Provider: "doppler-prod", Key: "TOKEN"} + + if _, err := r.Resolve(context.Background(), ws, ref); err != nil { + t.Fatalf("Resolve: %v", err) + } + r.InvalidateProvider(ws, "doppler-prod") + if _, err := r.Resolve(context.Background(), ws, ref); err != nil { + t.Fatalf("Resolve: %v", err) + } + + if got := store.getCalls.Load(); got != 2 { + t.Fatalf("store.Get called %d times, want 2 after invalidation", got) + } +} + +func TestResolverRejectsEmptyRefFields(t *testing.T) { + store := &mockStore{} + provider := &mockProvider{t: "doppler"} + r := newResolver(t, store, provider) + + cases := []SecretReference{ + {Provider: "", Key: "K"}, + {Provider: "p", Key: ""}, + } + for _, c := range cases { + if _, err := r.Resolve(context.Background(), uuid.New(), c); err == nil { + t.Fatalf("expected error for ref %+v", c) + } + } + if got := store.getCalls.Load(); got != 0 { + t.Fatalf("store should not be hit for invalid refs, got %d calls", got) + } +} + +func TestResolverNoFactoryRegistered(t *testing.T) { + ws := uuid.New() + store := &mockStore{ + configs: map[string]*ProviderConfig{ + "unknown": { + WorkspaceID: ws, + Name: "unknown", + Type: "vault", + Config: map[string]any{}, + }, + }, + } + r := NewResolver(store, NewRegistry(), NewCache(time.Minute)) + + _, err := r.Resolve(context.Background(), ws, SecretReference{Provider: "unknown", Key: "K"}) + if err == nil { + t.Fatal("expected error for unregistered provider type") + } +} + +func TestResolverProviderErrorPropagates(t *testing.T) { + ws := uuid.New() + store := &mockStore{ + configs: map[string]*ProviderConfig{ + "doppler-prod": { + WorkspaceID: ws, + Name: "doppler-prod", + Type: "doppler", + Config: map[string]any{}, + }, + }, + } + provider := &mockProvider{t: "doppler", resolveErr: errors.New("upstream 500")} + r := newResolver(t, store, provider) + + _, err := r.Resolve( + context.Background(), + ws, + SecretReference{Provider: "doppler-prod", Key: "K"}, + ) + if err == nil { + t.Fatal("expected error from provider to propagate") + } +} + +func TestResolverStoreErrorPropagates(t *testing.T) { + ws := uuid.New() + store := &mockStore{getErr: errors.New("db is sad")} + provider := &mockProvider{t: "doppler"} + r := newResolver(t, store, provider) + + _, err := r.Resolve( + context.Background(), + ws, + SecretReference{Provider: "doppler-prod", Key: "K"}, + ) + if err == nil { + t.Fatal("expected error from store to propagate") + } +} diff --git a/apps/workspace-engine/pkg/secrets/store.go b/apps/workspace-engine/pkg/secrets/store.go new file mode 100644 index 000000000..6a420cda1 --- /dev/null +++ b/apps/workspace-engine/pkg/secrets/store.go @@ -0,0 +1,100 @@ +package secrets + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/google/uuid" + "workspace-engine/pkg/crypto" + "workspace-engine/pkg/db" +) + +// Decryptor decrypts the bytea config payload stored on secret_provider rows. +// Matched 1:1 with the AES-256-CBC implementation used by @ctrlplane/secrets +// on the TypeScript side. +type Decryptor interface { + Decrypt(ciphertext string) (string, error) +} + +// PostgresStore loads secret_provider rows via sqlc and decrypts their +// configs in memory. +type PostgresStore struct { + queries *db.Queries + decryptor Decryptor +} + +// NewPostgresStore constructs a store using a sqlc Queries handle and a +// crypto.AES256CBC built from the workspace-engine's VARIABLES_AES_256_KEY. +func NewPostgresStore(queries *db.Queries, decryptor Decryptor) *PostgresStore { + return &PostgresStore{queries: queries, decryptor: decryptor} +} + +// NewPostgresStoreFromKey is a convenience constructor for callers that have a +// hex key rather than a Decryptor instance. +func NewPostgresStoreFromKey(queries *db.Queries, keyHex string) (*PostgresStore, error) { + dec, err := crypto.New(keyHex) + if err != nil { + return nil, fmt.Errorf("secrets: bad decryption key: %w", err) + } + return NewPostgresStore(queries, dec), nil +} + +func (s *PostgresStore) Get( + ctx context.Context, + workspaceID uuid.UUID, + providerName string, +) (*ProviderConfig, error) { + row, err := s.queries.GetSecretProviderByName(ctx, db.GetSecretProviderByNameParams{ + WorkspaceID: workspaceID, + Name: providerName, + }) + if err != nil { + return nil, fmt.Errorf( + "secrets: load provider %q for workspace %s: %w", + providerName, + workspaceID, + err, + ) + } + return s.toProviderConfig(row) +} + +func (s *PostgresStore) List( + ctx context.Context, + workspaceID uuid.UUID, +) ([]*ProviderConfig, error) { + rows, err := s.queries.ListSecretProvidersByWorkspaceID(ctx, workspaceID) + if err != nil { + return nil, fmt.Errorf("secrets: list providers for workspace %s: %w", workspaceID, err) + } + out := make([]*ProviderConfig, 0, len(rows)) + for _, row := range rows { + cfg, err := s.toProviderConfig(row) + if err != nil { + return nil, err + } + out = append(out, cfg) + } + return out, nil +} + +func (s *PostgresStore) toProviderConfig(row db.SecretProvider) (*ProviderConfig, error) { + // Ciphertext is the TS-encoded string (":") stored + // as bytea. Bytea -> []byte -> string with no transformation. + plaintext, err := s.decryptor.Decrypt(string(row.Config)) + if err != nil { + return nil, fmt.Errorf("secrets: decrypt config for %q: %w", row.Name, err) + } + cfg := make(map[string]any) + if err := json.Unmarshal([]byte(plaintext), &cfg); err != nil { + return nil, fmt.Errorf("secrets: parse decrypted config for %q: %w", row.Name, err) + } + return &ProviderConfig{ + ID: row.ID, + WorkspaceID: row.WorkspaceID, + Name: row.Name, + Type: string(row.Type), + Config: cfg, + }, nil +} diff --git a/apps/workspace-engine/pkg/secrets/types.go b/apps/workspace-engine/pkg/secrets/types.go new file mode 100644 index 000000000..bcedfe2cf --- /dev/null +++ b/apps/workspace-engine/pkg/secrets/types.go @@ -0,0 +1,54 @@ +// Package secrets resolves variable_value rows of kind = secret_ref by +// looking up the workspace's secret_provider entity, decrypting its +// configuration, and dispatching to a provider implementation (Doppler, AWS +// Secrets Manager, env, ...). The Resolver is constructed once at startup and +// injected into the variableresolver. +package secrets + +import ( + "context" + + "github.com/google/uuid" +) + +// SecretReference identifies a single secret value within a workspace. +type SecretReference struct { + // Provider is the workspace-unique name of the secret_provider row. + Provider string + // Path is provider-specific; may be empty. + Path string + // Key identifies the secret within Path. Some providers ignore Path and + // use Key alone (e.g. env). + Key string +} + +// Provider resolves a SecretReference against an external secret store. +// Implementations are constructed by a ProviderFactory from a decrypted +// ProviderConfig and are safe to reuse across resolutions for the lifetime of +// a single ProviderConfig row (TTL cache governs reuse). +type Provider interface { + // Type matches secret_provider.type. Used for registry lookups. + Type() string + // Resolve fetches the secret value. Returning a non-nil error blocks the + // downstream release dispatch. + Resolve(ctx context.Context, ref SecretReference) (string, error) +} + +// ProviderConfig is the decrypted view of a secret_provider row. +type ProviderConfig struct { + ID uuid.UUID + WorkspaceID uuid.UUID + Name string + Type string + // Config is the decrypted JSON payload, shape determined by Type. + Config map[string]any +} + +// ProviderFactory constructs a Provider from a decrypted config. +type ProviderFactory func(cfg map[string]any) (Provider, error) + +// ProviderConfigStore loads and decrypts secret_provider rows. +type ProviderConfigStore interface { + Get(ctx context.Context, workspaceID uuid.UUID, providerName string) (*ProviderConfig, error) + List(ctx context.Context, workspaceID uuid.UUID) ([]*ProviderConfig, error) +} From 9292ac8511920b26b1ce85577d508a283f8b015b Mon Sep 17 00:00:00 2001 From: Michael Leone Date: Mon, 11 May 2026 12:26:54 -0400 Subject: [PATCH 04/13] feat(secrets): add provider implementations for env, Doppler, AWS Secrets Manager - pkg/secrets/env: process-env provider with mandatory allowedKeys allowlist. Resolve hard-fails on any key not in the workspace's configured list before touching os.LookupEnv; mitigates the multi-tenant escape RFC 0006 flags. Lookup function is injectable for testing. - pkg/secrets/doppler: HTTP client against the v3 /configs/config/secret endpoint with a Bearer service token. Path parses to /; Key is the secret name. Prefers value.computed and falls back to value.raw. Tested with httptest.Server. - pkg/secrets/awssm: aws-sdk-go-v2 secretsmanager-backed provider. Path is the secret name or ARN; empty Key returns the raw SecretString while a non-empty Key extracts a JSON field via gjson (supports dotted paths). Static creds via accessKeyId/secretAccessKey, partial creds rejected, default credential chain otherwise. Tests use an injected client interface and make no real AWS calls. - pkg/secrets/providers: RegisterAll(*Registry) and NewDefaultRegistry() give main.go and tests a single seam to wire every built-in provider. - go.mod: aws-sdk-go-v2 secretsmanager + config + credentials, tidwall/gjson. --- apps/workspace-engine/go.mod | 18 ++ apps/workspace-engine/go.sum | 36 ++++ .../pkg/secrets/awssm/provider.go | 132 ++++++++++++++ .../pkg/secrets/awssm/provider_test.go | 165 ++++++++++++++++++ .../pkg/secrets/doppler/provider.go | 130 ++++++++++++++ .../pkg/secrets/doppler/provider_test.go | 157 +++++++++++++++++ .../pkg/secrets/env/provider.go | 62 +++++++ .../pkg/secrets/env/provider_test.go | 76 ++++++++ .../pkg/secrets/providers/providers.go | 27 +++ .../pkg/secrets/providers/providers_test.go | 21 +++ 10 files changed, 824 insertions(+) create mode 100644 apps/workspace-engine/pkg/secrets/awssm/provider.go create mode 100644 apps/workspace-engine/pkg/secrets/awssm/provider_test.go create mode 100644 apps/workspace-engine/pkg/secrets/doppler/provider.go create mode 100644 apps/workspace-engine/pkg/secrets/doppler/provider_test.go create mode 100644 apps/workspace-engine/pkg/secrets/env/provider.go create mode 100644 apps/workspace-engine/pkg/secrets/env/provider_test.go create mode 100644 apps/workspace-engine/pkg/secrets/providers/providers.go create mode 100644 apps/workspace-engine/pkg/secrets/providers/providers_test.go diff --git a/apps/workspace-engine/go.mod b/apps/workspace-engine/go.mod index 3914d8a96..b2f596f77 100644 --- a/apps/workspace-engine/go.mod +++ b/apps/workspace-engine/go.mod @@ -7,6 +7,10 @@ require ( github.com/argoproj/argo-cd/v3 v3.3.4 github.com/argoproj/argo-workflows/v4 v4.0.3 github.com/avast/retry-go v2.7.0+incompatible + github.com/aws/aws-sdk-go-v2 v1.41.7 + github.com/aws/aws-sdk-go-v2/config v1.32.17 + github.com/aws/aws-sdk-go-v2/credentials v1.19.16 + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.7 github.com/charmbracelet/log v0.4.2 github.com/confluentinc/confluent-kafka-go/v2 v2.13.3 github.com/dgraph-io/ristretto/v2 v2.3.0 @@ -29,6 +33,7 @@ require ( github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.1 github.com/teambition/rrule-go v1.8.2 + github.com/tidwall/gjson v1.19.0 go.opentelemetry.io/contrib/bridges/otelslog v0.13.0 go.opentelemetry.io/contrib/instrumentation/runtime v0.63.0 go.opentelemetry.io/otel v1.43.0 @@ -130,6 +135,17 @@ require ( github.com/argoproj/gitops-engine v0.7.1-0.20250908182407-97ad5b59a627 // indirect github.com/argoproj/pkg v0.13.7-0.20250123033407-65f2d4777bfd // indirect github.com/argoproj/pkg/v2 v2.0.1 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 // indirect + github.com/aws/smithy-go v1.25.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect @@ -280,6 +296,8 @@ require ( github.com/tchap/go-patricia/v2 v2.3.3 // indirect github.com/testcontainers/testcontainers-go v0.42.0 // indirect github.com/testcontainers/testcontainers-go/modules/compose v0.41.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect github.com/upper/db/v4 v4.10.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fastjson v1.6.7 // indirect diff --git a/apps/workspace-engine/go.sum b/apps/workspace-engine/go.sum index ff91c6536..38b80179e 100644 --- a/apps/workspace-engine/go.sum +++ b/apps/workspace-engine/go.sum @@ -78,6 +78,36 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/avast/retry-go v2.7.0+incompatible h1:XaGnzl7gESAideSjr+I8Hki/JBi+Yb9baHlMRPeSC84= github.com/avast/retry-go v2.7.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= +github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8= +github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc= +github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU= +github.com/aws/aws-sdk-go-v2/config v1.32.17/go.mod h1:OXqUMzgXytfoF9JaKkhrOYsyh72t9G+MJH8mMRaexOE= +github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU= +github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.7 h1:JUGKqUnJHbXpS8uyuICP/zpQ+vXUIXW2zTEqjMLCqrY= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.7/go.mod h1:l/cqI7ujYqBuTR6Ll13d9/gG/uUdlVzJ1UDltEEBTOo= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 h1:+1Kl1zx6bWi4X7cKi3VYh29h8BvsCoHQEQ6ST9X8w7w= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio= +github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI= +github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -905,6 +935,12 @@ github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44Xt github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= github.com/testcontainers/testcontainers-go/modules/compose v0.41.0 h1:6ttsQ6IilJYMoTFI2gu9l7KmKlnlY9XGkP0wtgh4rF4= github.com/testcontainers/testcontainers-go/modules/compose v0.41.0/go.mod h1:6PfaNLXsylvZE5CID8QMZ4fWjLHORvqm1xcGBncdzAY= +github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= +github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 h1:QB54BJwA6x8QU9nHY3xJSZR2kX9bgpZekRKGkLTmEXA= github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375/go.mod h1:xRroudyp5iVtxKqZCrA6n2TLFRBf8bmnjr1UD4x+z7g= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= diff --git a/apps/workspace-engine/pkg/secrets/awssm/provider.go b/apps/workspace-engine/pkg/secrets/awssm/provider.go new file mode 100644 index 000000000..5dbb70499 --- /dev/null +++ b/apps/workspace-engine/pkg/secrets/awssm/provider.go @@ -0,0 +1,132 @@ +// Package awssm implements a secrets.Provider backed by AWS Secrets Manager. +// +// SecretReference shape: +// +// Provider: secret_provider.name in the workspace +// Path: secret name or ARN (e.g. "prod/db" or +// "arn:aws:secretsmanager:us-east-1:123:secret:prod/db-AbCdEf") +// Key: optional. If empty, the full SecretString is returned. If set, +// the SecretString is treated as JSON and the named field is +// extracted via gjson. +package awssm + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + "github.com/tidwall/gjson" + "workspace-engine/pkg/secrets" +) + +const Type = "aws_secrets_manager" + +// secretsClient is the subset of secretsmanager.Client the provider uses. +// Tests substitute a fake implementation; production uses the real SDK client. +type secretsClient interface { + GetSecretValue( + ctx context.Context, + params *secretsmanager.GetSecretValueInput, + optFns ...func(*secretsmanager.Options), + ) (*secretsmanager.GetSecretValueOutput, error) +} + +type Provider struct { + client secretsClient +} + +// Factory matches secrets.ProviderFactory. The decrypted config supports: +// +// region (required) +// accessKeyId (optional) +// secretAccessKey (optional) +// +// When the static credentials are absent, the SDK default credential chain is +// used (instance role, IRSA, etc). +func Factory(cfg map[string]any) (secrets.Provider, error) { + region, ok := cfg["region"].(string) + if !ok || region == "" { + return nil, fmt.Errorf("awssm provider: region is required") + } + awsCfg, err := buildAWSConfig(context.Background(), region, cfg) + if err != nil { + return nil, err + } + return &Provider{client: secretsmanager.NewFromConfig(awsCfg)}, nil +} + +func buildAWSConfig( + ctx context.Context, + region string, + cfg map[string]any, +) (aws.Config, error) { + loadOpts := []func(*awsconfig.LoadOptions) error{awsconfig.WithRegion(region)} + + accessKeyID, hasAK := stringField(cfg, "accessKeyId") + secretKey, hasSK := stringField(cfg, "secretAccessKey") + switch { + case hasAK && hasSK: + loadOpts = append( + loadOpts, + awsconfig.WithCredentialsProvider( + credentials.NewStaticCredentialsProvider(accessKeyID, secretKey, ""), + ), + ) + case hasAK != hasSK: + return aws.Config{}, fmt.Errorf( + "awssm provider: accessKeyId and secretAccessKey must both be set or both omitted", + ) + } + + awsCfg, err := awsconfig.LoadDefaultConfig(ctx, loadOpts...) + if err != nil { + return aws.Config{}, fmt.Errorf("awssm provider: load AWS config: %w", err) + } + return awsCfg, nil +} + +func stringField(cfg map[string]any, key string) (string, bool) { + v, ok := cfg[key] + if !ok { + return "", false + } + s, ok := v.(string) + return s, ok && s != "" +} + +func (*Provider) Type() string { return Type } + +func (p *Provider) Resolve(ctx context.Context, ref secrets.SecretReference) (string, error) { + if ref.Path == "" { + return "", fmt.Errorf( + "awssm provider: SecretReference.Path is required (secret name or ARN)", + ) + } + + out, err := p.client.GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{ + SecretId: aws.String(ref.Path), + }) + if err != nil { + return "", fmt.Errorf("awssm provider: GetSecretValue %s: %w", ref.Path, err) + } + if out.SecretString == nil { + return "", fmt.Errorf("awssm provider: secret %s has no SecretString payload", ref.Path) + } + + if ref.Key == "" { + return *out.SecretString, nil + } + + r := gjson.Get(*out.SecretString, ref.Key) + if !r.Exists() { + return "", fmt.Errorf( + "awssm provider: secret %s has no JSON field %q", + ref.Path, + ref.Key, + ) + } + return r.String(), nil +} diff --git a/apps/workspace-engine/pkg/secrets/awssm/provider_test.go b/apps/workspace-engine/pkg/secrets/awssm/provider_test.go new file mode 100644 index 000000000..49e9578f8 --- /dev/null +++ b/apps/workspace-engine/pkg/secrets/awssm/provider_test.go @@ -0,0 +1,165 @@ +package awssm + +import ( + "context" + "errors" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + "workspace-engine/pkg/secrets" +) + +type fakeClient struct { + in *secretsmanager.GetSecretValueInput + out *secretsmanager.GetSecretValueOutput + err error +} + +func (f *fakeClient) GetSecretValue( + _ context.Context, + in *secretsmanager.GetSecretValueInput, + _ ...func(*secretsmanager.Options), +) (*secretsmanager.GetSecretValueOutput, error) { + f.in = in + if f.err != nil { + return nil, f.err + } + return f.out, nil +} + +func TestResolveReturnsRawSecretStringWhenKeyEmpty(t *testing.T) { + fc := &fakeClient{ + out: &secretsmanager.GetSecretValueOutput{ + SecretString: aws.String("raw-value"), + }, + } + p := &Provider{client: fc} + + got, err := p.Resolve(context.Background(), secrets.SecretReference{Path: "prod/db"}) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if got != "raw-value" { + t.Fatalf("got %q want raw-value", got) + } + if fc.in.SecretId == nil || *fc.in.SecretId != "prod/db" { + t.Fatalf("unexpected SecretId %v", fc.in.SecretId) + } +} + +func TestResolveExtractsJSONFieldByKey(t *testing.T) { + fc := &fakeClient{ + out: &secretsmanager.GetSecretValueOutput{ + SecretString: aws.String(`{"username":"app","password":"hunter2","nested":{"k":"v"}}`), + }, + } + p := &Provider{client: fc} + + got, err := p.Resolve(context.Background(), secrets.SecretReference{ + Path: "prod/db", + Key: "password", + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if got != "hunter2" { + t.Fatalf("got %q want hunter2", got) + } + + got, err = p.Resolve(context.Background(), secrets.SecretReference{ + Path: "prod/db", + Key: "nested.k", + }) + if err != nil { + t.Fatalf("Resolve nested: %v", err) + } + if got != "v" { + t.Fatalf("nested got %q want v", got) + } +} + +func TestResolveMissingJSONField(t *testing.T) { + fc := &fakeClient{ + out: &secretsmanager.GetSecretValueOutput{ + SecretString: aws.String(`{"username":"app"}`), + }, + } + p := &Provider{client: fc} + + _, err := p.Resolve(context.Background(), secrets.SecretReference{ + Path: "prod/db", + Key: "password", + }) + if err == nil { + t.Fatal("expected error for missing JSON field") + } +} + +func TestResolveEmptyPathRejected(t *testing.T) { + p := &Provider{client: &fakeClient{}} + _, err := p.Resolve(context.Background(), secrets.SecretReference{Key: "K"}) + if err == nil { + t.Fatal("expected error for empty Path") + } +} + +func TestResolveNoSecretStringPayload(t *testing.T) { + fc := &fakeClient{out: &secretsmanager.GetSecretValueOutput{}} + p := &Provider{client: fc} + _, err := p.Resolve(context.Background(), secrets.SecretReference{Path: "prod/db"}) + if err == nil { + t.Fatal("expected error when SecretString is nil") + } +} + +func TestResolveUpstreamErrorPropagates(t *testing.T) { + fc := &fakeClient{err: errors.New("AccessDenied")} + p := &Provider{client: fc} + _, err := p.Resolve(context.Background(), secrets.SecretReference{Path: "prod/db"}) + if err == nil { + t.Fatal("expected error to propagate") + } +} + +func TestFactoryRejectsBadConfigs(t *testing.T) { + cases := []struct { + name string + cfg map[string]any + }{ + {"missing region", map[string]any{}}, + {"empty region", map[string]any{"region": ""}}, + {"region wrong type", map[string]any{"region": 1}}, + { + "partial creds (key only)", + map[string]any{"region": "us-east-1", "accessKeyId": "AKIA..."}, + }, + { + "partial creds (secret only)", + map[string]any{"region": "us-east-1", "secretAccessKey": "secret"}, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + if _, err := Factory(c.cfg); err == nil { + t.Fatal("expected error") + } + }) + } +} + +func TestFactoryAcceptsRegionOnly(t *testing.T) { + if _, err := Factory(map[string]any{"region": "us-east-1"}); err != nil { + t.Fatalf("Factory: %v", err) + } +} + +func TestFactoryAcceptsStaticCreds(t *testing.T) { + if _, err := Factory(map[string]any{ + "region": "us-east-1", + "accessKeyId": "AKIAIOSFODNN7EXAMPLE", + "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + }); err != nil { + t.Fatalf("Factory: %v", err) + } +} diff --git a/apps/workspace-engine/pkg/secrets/doppler/provider.go b/apps/workspace-engine/pkg/secrets/doppler/provider.go new file mode 100644 index 000000000..b68d7c36c --- /dev/null +++ b/apps/workspace-engine/pkg/secrets/doppler/provider.go @@ -0,0 +1,130 @@ +// Package doppler implements a secrets.Provider backed by the Doppler v3 API. +// +// SecretReference shape for Doppler: +// +// Provider: secret_provider.name in the workspace +// Path: "/" (e.g. "backend/production") +// Key: Doppler secret name within the config (e.g. "ARGOCD_TOKEN") +// +// The provider talks to https://api.doppler.com/v3/configs/config/secret with +// a service-token Bearer header. +package doppler + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "workspace-engine/pkg/secrets" +) + +const ( + Type = "doppler" + defaultBaseURL = "https://api.doppler.com" + defaultTimeout = 10 * time.Second +) + +type Provider struct { + serviceToken string + baseURL string + client *http.Client +} + +// Factory matches secrets.ProviderFactory. +func Factory(cfg map[string]any) (secrets.Provider, error) { + tokenRaw, ok := cfg["serviceToken"] + if !ok { + return nil, fmt.Errorf("doppler provider: missing serviceToken") + } + token, ok := tokenRaw.(string) + if !ok || token == "" { + return nil, fmt.Errorf("doppler provider: serviceToken must be a non-empty string") + } + if !strings.HasPrefix(token, "dp.st.") { + return nil, fmt.Errorf("doppler provider: serviceToken must start with %q", "dp.st.") + } + return &Provider{ + serviceToken: token, + baseURL: defaultBaseURL, + client: &http.Client{Timeout: defaultTimeout}, + }, nil +} + +func (*Provider) Type() string { return Type } + +func (p *Provider) Resolve(ctx context.Context, ref secrets.SecretReference) (string, error) { + project, config, err := parsePath(ref.Path) + if err != nil { + return "", err + } + + u, err := url.Parse(p.baseURL + "/v3/configs/config/secret") + if err != nil { + return "", fmt.Errorf("doppler provider: bad baseURL: %w", err) + } + q := u.Query() + q.Set("project", project) + q.Set("config", config) + q.Set("name", ref.Key) + u.RawQuery = q.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + if err != nil { + return "", fmt.Errorf("doppler provider: build request: %w", err) + } + req.Header.Set("Authorization", "Bearer "+p.serviceToken) + req.Header.Set("Accept", "application/json") + + resp, err := p.client.Do(req) + if err != nil { + return "", fmt.Errorf("doppler provider: HTTP call: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf( + "doppler provider: secret %s/%s/%s lookup returned %d", + project, + config, + ref.Key, + resp.StatusCode, + ) + } + + var payload struct { + Value struct { + Computed string `json:"computed"` + Raw string `json:"raw"` + } `json:"value"` + } + if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { + return "", fmt.Errorf("doppler provider: decode response: %w", err) + } + if payload.Value.Computed != "" { + return payload.Value.Computed, nil + } + if payload.Value.Raw != "" { + return payload.Value.Raw, nil + } + return "", fmt.Errorf( + "doppler provider: secret %s/%s/%s has empty value", + project, + config, + ref.Key, + ) +} + +func parsePath(path string) (project, config string, err error) { + parts := strings.SplitN(path, "/", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", fmt.Errorf( + "doppler provider: path must be \"/\", got %q", + path, + ) + } + return parts[0], parts[1], nil +} diff --git a/apps/workspace-engine/pkg/secrets/doppler/provider_test.go b/apps/workspace-engine/pkg/secrets/doppler/provider_test.go new file mode 100644 index 000000000..f079ffcb4 --- /dev/null +++ b/apps/workspace-engine/pkg/secrets/doppler/provider_test.go @@ -0,0 +1,157 @@ +package doppler + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + "time" + + "workspace-engine/pkg/secrets" +) + +func newTestProvider(t *testing.T, srv *httptest.Server) *Provider { + t.Helper() + p, err := Factory(map[string]any{"serviceToken": "dp.st.test1234567890"}) + if err != nil { + t.Fatalf("Factory: %v", err) + } + prov := p.(*Provider) + prov.baseURL = srv.URL + prov.client = srv.Client() + prov.client.Timeout = 2 * time.Second + return prov +} + +func TestResolveHappyPath(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got := r.URL.Path; got != "/v3/configs/config/secret" { + t.Errorf("path %q want /v3/configs/config/secret", got) + } + q := r.URL.Query() + if q.Get("project") != "backend" || q.Get("config") != "production" || + q.Get("name") != "ARGOCD_TOKEN" { + t.Errorf("unexpected query %v", q) + } + if got := r.Header.Get("Authorization"); got != "Bearer dp.st.test1234567890" { + t.Errorf("auth header %q", got) + } + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte(`{"value":{"computed":"resolved-token","raw":"resolved-token"}}`), + ) + })) + defer srv.Close() + + p := newTestProvider(t, srv) + got, err := p.Resolve(context.Background(), secrets.SecretReference{ + Path: "backend/production", + Key: "ARGOCD_TOKEN", + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if got != "resolved-token" { + t.Fatalf("got %q want resolved-token", got) + } +} + +func TestResolveNon200(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer srv.Close() + + p := newTestProvider(t, srv) + _, err := p.Resolve( + context.Background(), + secrets.SecretReference{Path: "p/c", Key: "K"}, + ) + if err == nil { + t.Fatal("expected error on 404") + } +} + +func TestResolveEmptyValue(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte(`{"value":{"computed":"","raw":""}}`)) + })) + defer srv.Close() + + p := newTestProvider(t, srv) + _, err := p.Resolve( + context.Background(), + secrets.SecretReference{Path: "p/c", Key: "K"}, + ) + if err == nil { + t.Fatal("expected error on empty value") + } +} + +func TestResolveFallsBackToRaw(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte(`{"value":{"computed":"","raw":"raw-val"}}`)) + })) + defer srv.Close() + + p := newTestProvider(t, srv) + got, err := p.Resolve( + context.Background(), + secrets.SecretReference{Path: "p/c", Key: "K"}, + ) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if got != "raw-val" { + t.Fatalf("got %q want raw-val", got) + } +} + +func TestParsePath(t *testing.T) { + cases := []struct { + in string + ok bool + proj string + cfg string + }{ + {"backend/production", true, "backend", "production"}, + {"a/b", true, "a", "b"}, + {"single", false, "", ""}, + {"", false, "", ""}, + {"/missing-project", false, "", ""}, + {"missing-config/", false, "", ""}, + } + for _, c := range cases { + t.Run(c.in, func(t *testing.T) { + p, cfg, err := parsePath(c.in) + if c.ok && err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !c.ok && err == nil { + t.Fatal("expected error") + } + if c.ok && (p != c.proj || cfg != c.cfg) { + t.Fatalf("got (%q,%q) want (%q,%q)", p, cfg, c.proj, c.cfg) + } + }) + } +} + +func TestFactoryRejectsBadConfigs(t *testing.T) { + cases := []struct { + name string + cfg map[string]any + }{ + {"missing", map[string]any{}}, + {"wrong type", map[string]any{"serviceToken": 123}}, + {"empty", map[string]any{"serviceToken": ""}}, + {"bad prefix", map[string]any{"serviceToken": "not-a-doppler-token"}}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + if _, err := Factory(c.cfg); err == nil { + t.Fatal("expected error") + } + }) + } +} diff --git a/apps/workspace-engine/pkg/secrets/env/provider.go b/apps/workspace-engine/pkg/secrets/env/provider.go new file mode 100644 index 000000000..c8874c9ea --- /dev/null +++ b/apps/workspace-engine/pkg/secrets/env/provider.go @@ -0,0 +1,62 @@ +// Package env implements a secrets.Provider that reads from the +// workspace-engine process environment. Every workspace using this provider +// must list the permitted env var names explicitly in allowedKeys to prevent +// a tenant from reading arbitrary process state. +package env + +import ( + "context" + "fmt" + "os" + + "workspace-engine/pkg/secrets" +) + +const Type = "env" + +type Provider struct { + allowed map[string]struct{} + lookup func(string) (string, bool) +} + +// Factory matches secrets.ProviderFactory. The config must contain an +// allowedKeys array with at least one entry; the API and Drizzle validators +// enforce the shape, the factory enforces it again defensively. +func Factory(cfg map[string]any) (secrets.Provider, error) { + raw, ok := cfg["allowedKeys"] + if !ok { + return nil, fmt.Errorf("env provider: missing allowedKeys in config") + } + list, ok := raw.([]any) + if !ok { + return nil, fmt.Errorf("env provider: allowedKeys must be a JSON array, got %T", raw) + } + if len(list) == 0 { + return nil, fmt.Errorf("env provider: allowedKeys is empty") + } + allowed := make(map[string]struct{}, len(list)) + for _, v := range list { + s, ok := v.(string) + if !ok || s == "" { + return nil, fmt.Errorf( + "env provider: allowedKeys entries must be non-empty strings, got %T", + v, + ) + } + allowed[s] = struct{}{} + } + return &Provider{allowed: allowed, lookup: os.LookupEnv}, nil +} + +func (*Provider) Type() string { return Type } + +func (p *Provider) Resolve(_ context.Context, ref secrets.SecretReference) (string, error) { + if _, ok := p.allowed[ref.Key]; !ok { + return "", fmt.Errorf("env provider: key %q not in allowlist", ref.Key) + } + v, ok := p.lookup(ref.Key) + if !ok { + return "", fmt.Errorf("env provider: env var %q is not set", ref.Key) + } + return v, nil +} diff --git a/apps/workspace-engine/pkg/secrets/env/provider_test.go b/apps/workspace-engine/pkg/secrets/env/provider_test.go new file mode 100644 index 000000000..87f1dff1d --- /dev/null +++ b/apps/workspace-engine/pkg/secrets/env/provider_test.go @@ -0,0 +1,76 @@ +package env + +import ( + "context" + "testing" + + "workspace-engine/pkg/secrets" +) + +func newTestProvider(t *testing.T, cfg map[string]any, envVars map[string]string) *Provider { + t.Helper() + p, err := Factory(cfg) + if err != nil { + t.Fatalf("Factory: %v", err) + } + prov := p.(*Provider) + prov.lookup = func(k string) (string, bool) { + v, ok := envVars[k] + return v, ok + } + return prov +} + +func TestResolveHappyPath(t *testing.T) { + p := newTestProvider(t, + map[string]any{"allowedKeys": []any{"FOO", "BAR"}}, + map[string]string{"FOO": "value-foo"}, + ) + got, err := p.Resolve(context.Background(), secrets.SecretReference{Key: "FOO"}) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if got != "value-foo" { + t.Fatalf("got %q want value-foo", got) + } +} + +func TestResolveRejectsNotInAllowlist(t *testing.T) { + p := newTestProvider(t, + map[string]any{"allowedKeys": []any{"FOO"}}, + map[string]string{"FOO": "x", "BAR": "y"}, + ) + if _, err := p.Resolve(context.Background(), secrets.SecretReference{Key: "BAR"}); err == nil { + t.Fatal("expected allowlist rejection") + } +} + +func TestResolveMissingEnvVar(t *testing.T) { + p := newTestProvider(t, + map[string]any{"allowedKeys": []any{"FOO"}}, + map[string]string{}, + ) + if _, err := p.Resolve(context.Background(), secrets.SecretReference{Key: "FOO"}); err == nil { + t.Fatal("expected error for unset env var") + } +} + +func TestFactoryRejectsBadConfigs(t *testing.T) { + cases := []struct { + name string + cfg map[string]any + }{ + {"missing", map[string]any{}}, + {"wrong type", map[string]any{"allowedKeys": "FOO"}}, + {"empty list", map[string]any{"allowedKeys": []any{}}}, + {"non-string entry", map[string]any{"allowedKeys": []any{"FOO", 42}}}, + {"empty string entry", map[string]any{"allowedKeys": []any{""}}}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + if _, err := Factory(c.cfg); err == nil { + t.Fatal("expected error") + } + }) + } +} diff --git a/apps/workspace-engine/pkg/secrets/providers/providers.go b/apps/workspace-engine/pkg/secrets/providers/providers.go new file mode 100644 index 000000000..bf41dd7be --- /dev/null +++ b/apps/workspace-engine/pkg/secrets/providers/providers.go @@ -0,0 +1,27 @@ +// Package providers wires the built-in secret provider implementations into +// a secrets.Registry. main.go calls RegisterAll once during startup; new +// provider types are added here. +package providers + +import ( + "workspace-engine/pkg/secrets" + "workspace-engine/pkg/secrets/awssm" + "workspace-engine/pkg/secrets/doppler" + "workspace-engine/pkg/secrets/env" +) + +// RegisterAll registers every provider implementation shipped with +// workspace-engine. Callers may add additional registrations afterward. +func RegisterAll(r *secrets.Registry) { + r.Register(awssm.Type, awssm.Factory) + r.Register(doppler.Type, doppler.Factory) + r.Register(env.Type, env.Factory) +} + +// NewDefaultRegistry constructs a Registry pre-populated with the built-in +// providers. Convenience for tests and main.go. +func NewDefaultRegistry() *secrets.Registry { + r := secrets.NewRegistry() + RegisterAll(r) + return r +} diff --git a/apps/workspace-engine/pkg/secrets/providers/providers_test.go b/apps/workspace-engine/pkg/secrets/providers/providers_test.go new file mode 100644 index 000000000..620a2ee96 --- /dev/null +++ b/apps/workspace-engine/pkg/secrets/providers/providers_test.go @@ -0,0 +1,21 @@ +package providers + +import ( + "sort" + "testing" +) + +func TestRegisterAllRegistersExpectedTypes(t *testing.T) { + r := NewDefaultRegistry() + got := r.Types() + sort.Strings(got) + want := []string{"aws_secrets_manager", "doppler", "env"} + if len(got) != len(want) { + t.Fatalf("got %v, want %v", got, want) + } + for i, w := range want { + if got[i] != w { + t.Fatalf("got %v, want %v", got, want) + } + } +} From 97ebb5dd64ec752d0bb13bda921eaffc0c8544d7 Mon Sep 17 00:00:00 2001 From: Michael Leone Date: Mon, 11 May 2026 12:52:33 -0400 Subject: [PATCH 05/13] feat(secrets): wire secret_ref resolution end-to-end - oapi: new SecretReferenceValue (secretProvider, secretKey, secretPath) added to the Value oneOf. (*Value).GetType returns "secret_ref" when the provider/key fields are populated; regenerated oapi types include AsSecretReferenceValue / FromSecretReferenceValue. - pkg/db/convert.go: the secret_ref kind path no longer errors. It now builds a SecretReferenceValue from the row's secret_provider/secret_key/ secret_path columns. Test updated from "unsupported" to a positive roundtrip. - variableresolver: new SecretResolver interface (satisfied by *secrets.Resolver). ResolveValue takes a secretResolver and workspaceID; the new "secret_ref" case joins secret_path with "/" for provider-native path encoding, calls the resolver, and returns the plaintext wrapped as a LiteralValue so it flows into release.Variables like any other value. Resolve and its three helpers thread the resolver + workspace through. - desiredrelease: reconciler holds *secrets.Resolver, threaded into variableresolver.Resolve. Reconcile, NewController, and New all accept a resolver; controller wires it into Process. deploymentplan does the same via PostgresVarResolver. - main.go: newSecretResolver assembles PostgresStore (AES-256-CBC decryptor from VARIABLES_AES_256_KEY) + default provider registry + value cache (SECRETS_CACHE_TTL, default 5m). When VARIABLES_AES_256_KEY is unset the resolver is nil and any secret_ref encountered surfaces a clear error at resolve time without crashing the engine. - config: VariablesAes256Key (VARIABLES_AES_256_KEY) and SecretsCacheTTL (SECRETS_CACHE_TTL) added to the env-driven Config. --- apps/workspace-engine/main.go | 32 ++++- apps/workspace-engine/oapi/openapi.json | 27 ++++ .../oapi/spec/schemas/core.jsonnet | 25 ++++ apps/workspace-engine/pkg/config/env.go | 8 ++ apps/workspace-engine/pkg/db/convert.go | 12 +- apps/workspace-engine/pkg/db/convert_test.go | 14 +- apps/workspace-engine/pkg/oapi/oapi.gen.go | 38 ++++++ apps/workspace-engine/pkg/oapi/oapi.go | 8 ++ .../pkg/secrets/awssm/provider.go | 77 +++++------ .../pkg/secrets/awssm/provider_test.go | 39 ++++-- .../pkg/secrets/doppler/provider.go | 34 +++-- .../pkg/secrets/doppler/provider_test.go | 16 ++- .../pkg/secrets/env/provider.go | 52 +++---- .../pkg/secrets/env/provider_test.go | 29 ++-- .../pkg/secrets/registry_test.go | 13 +- .../pkg/secrets/resolver_test.go | 16 ++- apps/workspace-engine/pkg/secrets/store.go | 7 +- apps/workspace-engine/pkg/secrets/types.go | 12 +- .../controllers/deploymentplan/controller.go | 9 +- .../deploymentplan/getters_postgres.go | 19 ++- .../controllers/desiredrelease/controller.go | 21 +-- .../controllers/desiredrelease/reconcile.go | 19 ++- .../desiredrelease/reconcile_test.go | 12 +- .../variableresolver/resolve.go | 43 +++++- .../variableresolver/resolve_test.go | 128 ++++++++++++++++-- .../desiredrelease/variableresolver/value.go | 58 +++++++- .../test/controllers/harness/pipeline.go | 2 +- 27 files changed, 589 insertions(+), 181 deletions(-) diff --git a/apps/workspace-engine/main.go b/apps/workspace-engine/main.go index 03009295b..5fac280b0 100644 --- a/apps/workspace-engine/main.go +++ b/apps/workspace-engine/main.go @@ -11,6 +11,8 @@ import ( "github.com/google/uuid" "workspace-engine/pkg/config" "workspace-engine/pkg/db" + "workspace-engine/pkg/secrets" + "workspace-engine/pkg/secrets/providers" "workspace-engine/svc" "workspace-engine/svc/claimcleanup" "workspace-engine/svc/controllers/deploymentplan" @@ -49,12 +51,14 @@ func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + secretResolver := newSecretResolver(ctx) + allServices := []svc.Service{ pprof.New(pprof.DefaultAddr(config.Global.PprofPort)), httpsvc.New(config.Global, db.GetPool(ctx)), claimcleanup.New(db.GetPool(ctx), 30*time.Second), - deploymentplan.New(WorkerID, db.GetPool(ctx)), + deploymentplan.New(WorkerID, db.GetPool(ctx), secretResolver), deploymentplanresult.New(WorkerID, db.GetPool(ctx)), deploymentresourceselectoreval.New(WorkerID, db.GetPool(ctx)), environmentresourceselectoreval.New(WorkerID, db.GetPool(ctx)), @@ -63,7 +67,7 @@ func main() { jobeligibility.New(WorkerID, db.GetPool(ctx)), jobverificationmetric.New(WorkerID, db.GetPool(ctx)), relationshipeval.New(WorkerID, db.GetPool(ctx)), - desiredrelease.New(WorkerID, db.GetPool(ctx)), + desiredrelease.New(WorkerID, db.GetPool(ctx), secretResolver), policyeval.New(WorkerID, db.GetPool(ctx)), } @@ -94,3 +98,27 @@ func main() { slog.Info("Workspace engine shut down") } + +// newSecretResolver wires the components needed to resolve secret_ref +// variable values. If VARIABLES_AES_256_KEY is unset the resolver is nil and +// any secret_ref encountered during reconciliation will block release +// dispatch with a clear error. +func newSecretResolver(ctx context.Context) *secrets.Resolver { + keyHex := config.Global.VariablesAes256Key + if keyHex == "" { + slog.Warn( + "VARIABLES_AES_256_KEY is unset; secret_ref variable values will fail to resolve", + ) + return nil + } + store, err := secrets.NewPostgresStoreFromKey(db.GetQueries(ctx), keyHex) + if err != nil { + slog.Error("Failed to construct secret store", "error", err) + os.Exit(1) + } + return secrets.NewResolver( + store, + providers.NewDefaultRegistry(), + secrets.NewCache(config.Global.SecretsCacheTTL), + ) +} diff --git a/apps/workspace-engine/oapi/openapi.json b/apps/workspace-engine/oapi/openapi.json index da8046c08..967b1f033 100644 --- a/apps/workspace-engine/oapi/openapi.json +++ b/apps/workspace-engine/oapi/openapi.json @@ -2507,6 +2507,30 @@ ], "type": "object" }, + "SecretReferenceValue": { + "properties": { + "secretKey": { + "description": "Secret key within the provider", + "type": "string" + }, + "secretPath": { + "description": "Optional provider-specific path components", + "items": { + "type": "string" + }, + "type": "array" + }, + "secretProvider": { + "description": "Workspace-unique secret_provider.name", + "type": "string" + } + }, + "required": [ + "secretProvider", + "secretKey" + ], + "type": "object" + }, "SensitiveValue": { "properties": { "valueHash": { @@ -2730,6 +2754,9 @@ }, { "$ref": "#/components/schemas/SensitiveValue" + }, + { + "$ref": "#/components/schemas/SecretReferenceValue" } ] }, diff --git a/apps/workspace-engine/oapi/spec/schemas/core.jsonnet b/apps/workspace-engine/oapi/spec/schemas/core.jsonnet index 510c7f388..1e79cffb5 100644 --- a/apps/workspace-engine/oapi/spec/schemas/core.jsonnet +++ b/apps/workspace-engine/oapi/spec/schemas/core.jsonnet @@ -74,11 +74,36 @@ local openapi = import '../lib/openapi.libsonnet'; }, }, + // SecretReferenceValue identifies a secret stored in an external provider. + // Resolution is performed at release time by the secrets resolver and the + // returned value flows through release.Variables as a LiteralValue. The + // plaintext is never persisted on the resolved Value. + SecretReferenceValue: { + type: 'object', + required: ['secretProvider', 'secretKey'], + properties: { + secretProvider: { + type: 'string', + description: 'Workspace-unique secret_provider.name', + }, + secretKey: { + type: 'string', + description: 'Secret key within the provider', + }, + secretPath: { + type: 'array', + items: { type: 'string' }, + description: 'Optional provider-specific path components', + }, + }, + }, + Value: { oneOf: [ openapi.schemaRef('LiteralValue'), openapi.schemaRef('ReferenceValue'), openapi.schemaRef('SensitiveValue'), + openapi.schemaRef('SecretReferenceValue'), ], }, diff --git a/apps/workspace-engine/pkg/config/env.go b/apps/workspace-engine/pkg/config/env.go index d40480829..3a4485a08 100644 --- a/apps/workspace-engine/pkg/config/env.go +++ b/apps/workspace-engine/pkg/config/env.go @@ -5,6 +5,7 @@ import ( "runtime" "strconv" "strings" + "time" "github.com/kelseyhightower/envconfig" ) @@ -53,6 +54,13 @@ type Config struct { // Whether to enable dry run for workflow jobs. DryRunEnabled bool `default:"false" envconfig:"DRY_RUN_ENABLED"` + + // Symmetric key used to encrypt/decrypt secret_provider.config rows. + // Must match @ctrlplane/secrets in the TypeScript layer (64 hex chars). + VariablesAes256Key string `default:"" envconfig:"VARIABLES_AES_256_KEY"` + + // TTL for the secrets resolver value cache. + SecretsCacheTTL time.Duration `default:"5m" envconfig:"SECRETS_CACHE_TTL"` } // GetMaxConcurrency returns the max concurrency for a given service kind. diff --git a/apps/workspace-engine/pkg/db/convert.go b/apps/workspace-engine/pkg/db/convert.go index e566bba14..a93bde8f2 100644 --- a/apps/workspace-engine/pkg/db/convert.go +++ b/apps/workspace-engine/pkg/db/convert.go @@ -324,7 +324,17 @@ func flattenVariableValue(r VariableValueAggRow) (oapi.Value, error) { return v, err } case "secret_ref": - return v, fmt.Errorf("secret_ref variable values are not yet supported") + sr := oapi.SecretReferenceValue{ + SecretProvider: derefString(r.SecretProvider), + SecretKey: derefString(r.SecretKey), + } + if len(r.SecretPath) > 0 { + path := append([]string(nil), r.SecretPath...) + sr.SecretPath = &path + } + if err := v.FromSecretReferenceValue(sr); err != nil { + return v, err + } default: return v, fmt.Errorf("unknown variable_value kind: %q", r.Kind) } diff --git a/apps/workspace-engine/pkg/db/convert_test.go b/apps/workspace-engine/pkg/db/convert_test.go index 5412c9b84..b70e687d6 100644 --- a/apps/workspace-engine/pkg/db/convert_test.go +++ b/apps/workspace-engine/pkg/db/convert_test.go @@ -262,9 +262,10 @@ func TestFlattenVariableValue_Ref(t *testing.T) { assert.Equal(t, []string{"host"}, rv.Path) } -func TestFlattenVariableValue_SecretRefUnsupported(t *testing.T) { +func TestFlattenVariableValue_SecretRef(t *testing.T) { provider := "vault" key := "kv/data/prod/db" + path := []string{"backend", "production"} agg := VariableValueAggRow{ ID: uuid.New(), VariableID: uuid.New(), @@ -272,9 +273,16 @@ func TestFlattenVariableValue_SecretRefUnsupported(t *testing.T) { Kind: "secret_ref", SecretProvider: &provider, SecretKey: &key, + SecretPath: path, } - _, err := flattenVariableValue(agg) - require.Error(t, err) + v, err := flattenVariableValue(agg) + require.NoError(t, err) + srv, err := v.AsSecretReferenceValue() + require.NoError(t, err) + assert.Equal(t, provider, srv.SecretProvider) + assert.Equal(t, key, srv.SecretKey) + require.NotNil(t, srv.SecretPath) + assert.Equal(t, path, *srv.SecretPath) } func TestToOapiDeploymentVariableValueFromAgg_CELSelector(t *testing.T) { diff --git a/apps/workspace-engine/pkg/oapi/oapi.gen.go b/apps/workspace-engine/pkg/oapi/oapi.gen.go index 18289aa24..101cdf01d 100644 --- a/apps/workspace-engine/pkg/oapi/oapi.gen.go +++ b/apps/workspace-engine/pkg/oapi/oapi.gen.go @@ -1156,6 +1156,18 @@ type RuleEvaluation struct { // RuleEvaluationActionType Type of action required type RuleEvaluationActionType string +// SecretReferenceValue defines model for SecretReferenceValue. +type SecretReferenceValue struct { + // SecretKey Secret key within the provider + SecretKey string `json:"secretKey"` + + // SecretPath Optional provider-specific path components + SecretPath *[]string `json:"secretPath,omitempty"` + + // SecretProvider Workspace-unique secret_provider.name + SecretProvider string `json:"secretProvider"` +} + // SensitiveValue defines model for SensitiveValue. type SensitiveValue struct { ValueHash string `json:"valueHash"` @@ -2314,6 +2326,32 @@ func (t *Value) MergeSensitiveValue(v SensitiveValue) error { return err } +// AsSecretReferenceValue returns the union data inside the Value as a SecretReferenceValue +func (t Value) AsSecretReferenceValue() (SecretReferenceValue, error) { + var body SecretReferenceValue + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromSecretReferenceValue overwrites any union data inside the Value as the provided SecretReferenceValue +func (t *Value) FromSecretReferenceValue(v SecretReferenceValue) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeSecretReferenceValue performs a merge with any union data inside the Value, using the provided SecretReferenceValue +func (t *Value) MergeSecretReferenceValue(v SecretReferenceValue) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + func (t Value) MarshalJSON() ([]byte, error) { b, err := t.union.MarshalJSON() return b, err diff --git a/apps/workspace-engine/pkg/oapi/oapi.go b/apps/workspace-engine/pkg/oapi/oapi.go index ad406d8a1..f718fa5df 100644 --- a/apps/workspace-engine/pkg/oapi/oapi.go +++ b/apps/workspace-engine/pkg/oapi/oapi.go @@ -94,6 +94,14 @@ func (j *Job) IsInTerminalState() bool { } func (v *Value) GetType() (string, error) { + // Try SecretReferenceValue first — its required field set is the most + // specific so a positive match leaves no ambiguity. + if srv, err := v.AsSecretReferenceValue(); err == nil { + if srv.SecretProvider != "" && srv.SecretKey != "" { + return "secret_ref", nil + } + } + // Try ReferenceValue - check that required fields are present if rv, err := v.AsReferenceValue(); err == nil { if rv.Reference != "" && rv.Path != nil { diff --git a/apps/workspace-engine/pkg/secrets/awssm/provider.go b/apps/workspace-engine/pkg/secrets/awssm/provider.go index 5dbb70499..38cfa9146 100644 --- a/apps/workspace-engine/pkg/secrets/awssm/provider.go +++ b/apps/workspace-engine/pkg/secrets/awssm/provider.go @@ -7,11 +7,12 @@ // "arn:aws:secretsmanager:us-east-1:123:secret:prod/db-AbCdEf") // Key: optional. If empty, the full SecretString is returned. If set, // the SecretString is treated as JSON and the named field is -// extracted via gjson. +// extracted via gjson (dotted paths supported). package awssm import ( "context" + "encoding/json" "fmt" "github.com/aws/aws-sdk-go-v2/aws" @@ -24,6 +25,28 @@ import ( const Type = "aws_secrets_manager" +// Config is the decrypted config payload for an aws_secrets_manager row. +// AccessKeyID + SecretAccessKey are both optional, but if one is set the +// other must be too. When both are absent the SDK's default credential chain +// is used (IRSA / instance role / shared config / env). +type Config struct { + Region string `json:"region"` + AccessKeyID string `json:"accessKeyId,omitempty"` + SecretAccessKey string `json:"secretAccessKey,omitempty"` +} + +func (c Config) validate() error { + if c.Region == "" { + return fmt.Errorf("awssm provider: region is required") + } + if (c.AccessKeyID == "") != (c.SecretAccessKey == "") { + return fmt.Errorf( + "awssm provider: accessKeyId and secretAccessKey must both be set or both omitted", + ) + } + return nil +} + // secretsClient is the subset of secretsmanager.Client the provider uses. // Tests substitute a fake implementation; production uses the real SDK client. type secretsClient interface { @@ -38,49 +61,32 @@ type Provider struct { client secretsClient } -// Factory matches secrets.ProviderFactory. The decrypted config supports: -// -// region (required) -// accessKeyId (optional) -// secretAccessKey (optional) -// -// When the static credentials are absent, the SDK default credential chain is -// used (instance role, IRSA, etc). -func Factory(cfg map[string]any) (secrets.Provider, error) { - region, ok := cfg["region"].(string) - if !ok || region == "" { - return nil, fmt.Errorf("awssm provider: region is required") +// Factory matches secrets.ProviderFactory. +func Factory(raw json.RawMessage) (secrets.Provider, error) { + var cfg Config + if err := json.Unmarshal(raw, &cfg); err != nil { + return nil, fmt.Errorf("awssm provider: parse config: %w", err) } - awsCfg, err := buildAWSConfig(context.Background(), region, cfg) + if err := cfg.validate(); err != nil { + return nil, err + } + awsCfg, err := buildAWSConfig(context.Background(), cfg) if err != nil { return nil, err } return &Provider{client: secretsmanager.NewFromConfig(awsCfg)}, nil } -func buildAWSConfig( - ctx context.Context, - region string, - cfg map[string]any, -) (aws.Config, error) { - loadOpts := []func(*awsconfig.LoadOptions) error{awsconfig.WithRegion(region)} - - accessKeyID, hasAK := stringField(cfg, "accessKeyId") - secretKey, hasSK := stringField(cfg, "secretAccessKey") - switch { - case hasAK && hasSK: +func buildAWSConfig(ctx context.Context, cfg Config) (aws.Config, error) { + loadOpts := []func(*awsconfig.LoadOptions) error{awsconfig.WithRegion(cfg.Region)} + if cfg.AccessKeyID != "" && cfg.SecretAccessKey != "" { loadOpts = append( loadOpts, awsconfig.WithCredentialsProvider( - credentials.NewStaticCredentialsProvider(accessKeyID, secretKey, ""), + credentials.NewStaticCredentialsProvider(cfg.AccessKeyID, cfg.SecretAccessKey, ""), ), ) - case hasAK != hasSK: - return aws.Config{}, fmt.Errorf( - "awssm provider: accessKeyId and secretAccessKey must both be set or both omitted", - ) } - awsCfg, err := awsconfig.LoadDefaultConfig(ctx, loadOpts...) if err != nil { return aws.Config{}, fmt.Errorf("awssm provider: load AWS config: %w", err) @@ -88,15 +94,6 @@ func buildAWSConfig( return awsCfg, nil } -func stringField(cfg map[string]any, key string) (string, bool) { - v, ok := cfg[key] - if !ok { - return "", false - } - s, ok := v.(string) - return s, ok && s != "" -} - func (*Provider) Type() string { return Type } func (p *Provider) Resolve(ctx context.Context, ref secrets.SecretReference) (string, error) { diff --git a/apps/workspace-engine/pkg/secrets/awssm/provider_test.go b/apps/workspace-engine/pkg/secrets/awssm/provider_test.go index 49e9578f8..75589e969 100644 --- a/apps/workspace-engine/pkg/secrets/awssm/provider_test.go +++ b/apps/workspace-engine/pkg/secrets/awssm/provider_test.go @@ -2,6 +2,7 @@ package awssm import ( "context" + "encoding/json" "errors" "testing" @@ -28,6 +29,15 @@ func (f *fakeClient) GetSecretValue( return f.out, nil } +func mustMarshal(t *testing.T, cfg Config) json.RawMessage { + t.Helper() + raw, err := json.Marshal(cfg) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + return raw +} + func TestResolveReturnsRawSecretStringWhenKeyEmpty(t *testing.T) { fc := &fakeClient{ out: &secretsmanager.GetSecretValueOutput{ @@ -51,7 +61,9 @@ func TestResolveReturnsRawSecretStringWhenKeyEmpty(t *testing.T) { func TestResolveExtractsJSONFieldByKey(t *testing.T) { fc := &fakeClient{ out: &secretsmanager.GetSecretValueOutput{ - SecretString: aws.String(`{"username":"app","password":"hunter2","nested":{"k":"v"}}`), + SecretString: aws.String( + `{"username":"app","password":"hunter2","nested":{"k":"v"}}`, + ), }, } p := &Provider{client: fc} @@ -125,23 +137,22 @@ func TestResolveUpstreamErrorPropagates(t *testing.T) { func TestFactoryRejectsBadConfigs(t *testing.T) { cases := []struct { name string - cfg map[string]any + raw json.RawMessage }{ - {"missing region", map[string]any{}}, - {"empty region", map[string]any{"region": ""}}, - {"region wrong type", map[string]any{"region": 1}}, + {"not json", json.RawMessage(`not-json`)}, + {"missing region", mustMarshal(t, Config{})}, { "partial creds (key only)", - map[string]any{"region": "us-east-1", "accessKeyId": "AKIA..."}, + mustMarshal(t, Config{Region: "us-east-1", AccessKeyID: "AKIA..."}), }, { "partial creds (secret only)", - map[string]any{"region": "us-east-1", "secretAccessKey": "secret"}, + mustMarshal(t, Config{Region: "us-east-1", SecretAccessKey: "secret"}), }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { - if _, err := Factory(c.cfg); err == nil { + if _, err := Factory(c.raw); err == nil { t.Fatal("expected error") } }) @@ -149,17 +160,17 @@ func TestFactoryRejectsBadConfigs(t *testing.T) { } func TestFactoryAcceptsRegionOnly(t *testing.T) { - if _, err := Factory(map[string]any{"region": "us-east-1"}); err != nil { + if _, err := Factory(mustMarshal(t, Config{Region: "us-east-1"})); err != nil { t.Fatalf("Factory: %v", err) } } func TestFactoryAcceptsStaticCreds(t *testing.T) { - if _, err := Factory(map[string]any{ - "region": "us-east-1", - "accessKeyId": "AKIAIOSFODNN7EXAMPLE", - "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", - }); err != nil { + if _, err := Factory(mustMarshal(t, Config{ + Region: "us-east-1", + AccessKeyID: "AKIAIOSFODNN7EXAMPLE", + SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + })); err != nil { t.Fatalf("Factory: %v", err) } } diff --git a/apps/workspace-engine/pkg/secrets/doppler/provider.go b/apps/workspace-engine/pkg/secrets/doppler/provider.go index b68d7c36c..6cb76a8a0 100644 --- a/apps/workspace-engine/pkg/secrets/doppler/provider.go +++ b/apps/workspace-engine/pkg/secrets/doppler/provider.go @@ -26,8 +26,24 @@ const ( Type = "doppler" defaultBaseURL = "https://api.doppler.com" defaultTimeout = 10 * time.Second + tokenPrefix = "dp.st." ) +// Config is the decrypted config payload for a doppler provider row. +type Config struct { + ServiceToken string `json:"serviceToken"` +} + +func (c Config) validate() error { + if c.ServiceToken == "" { + return fmt.Errorf("doppler provider: serviceToken is required") + } + if !strings.HasPrefix(c.ServiceToken, tokenPrefix) { + return fmt.Errorf("doppler provider: serviceToken must start with %q", tokenPrefix) + } + return nil +} + type Provider struct { serviceToken string baseURL string @@ -35,20 +51,16 @@ type Provider struct { } // Factory matches secrets.ProviderFactory. -func Factory(cfg map[string]any) (secrets.Provider, error) { - tokenRaw, ok := cfg["serviceToken"] - if !ok { - return nil, fmt.Errorf("doppler provider: missing serviceToken") - } - token, ok := tokenRaw.(string) - if !ok || token == "" { - return nil, fmt.Errorf("doppler provider: serviceToken must be a non-empty string") +func Factory(raw json.RawMessage) (secrets.Provider, error) { + var cfg Config + if err := json.Unmarshal(raw, &cfg); err != nil { + return nil, fmt.Errorf("doppler provider: parse config: %w", err) } - if !strings.HasPrefix(token, "dp.st.") { - return nil, fmt.Errorf("doppler provider: serviceToken must start with %q", "dp.st.") + if err := cfg.validate(); err != nil { + return nil, err } return &Provider{ - serviceToken: token, + serviceToken: cfg.ServiceToken, baseURL: defaultBaseURL, client: &http.Client{Timeout: defaultTimeout}, }, nil diff --git a/apps/workspace-engine/pkg/secrets/doppler/provider_test.go b/apps/workspace-engine/pkg/secrets/doppler/provider_test.go index f079ffcb4..553551374 100644 --- a/apps/workspace-engine/pkg/secrets/doppler/provider_test.go +++ b/apps/workspace-engine/pkg/secrets/doppler/provider_test.go @@ -2,6 +2,7 @@ package doppler import ( "context" + "encoding/json" "net/http" "net/http/httptest" "testing" @@ -12,7 +13,8 @@ import ( func newTestProvider(t *testing.T, srv *httptest.Server) *Provider { t.Helper() - p, err := Factory(map[string]any{"serviceToken": "dp.st.test1234567890"}) + raw, _ := json.Marshal(Config{ServiceToken: "dp.st.test1234567890"}) + p, err := Factory(raw) if err != nil { t.Fatalf("Factory: %v", err) } @@ -140,16 +142,16 @@ func TestParsePath(t *testing.T) { func TestFactoryRejectsBadConfigs(t *testing.T) { cases := []struct { name string - cfg map[string]any + raw string }{ - {"missing", map[string]any{}}, - {"wrong type", map[string]any{"serviceToken": 123}}, - {"empty", map[string]any{"serviceToken": ""}}, - {"bad prefix", map[string]any{"serviceToken": "not-a-doppler-token"}}, + {"not json", `not-json`}, + {"missing", `{}`}, + {"empty", `{"serviceToken":""}`}, + {"bad prefix", `{"serviceToken":"not-a-doppler-token"}`}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { - if _, err := Factory(c.cfg); err == nil { + if _, err := Factory([]byte(c.raw)); err == nil { t.Fatal("expected error") } }) diff --git a/apps/workspace-engine/pkg/secrets/env/provider.go b/apps/workspace-engine/pkg/secrets/env/provider.go index c8874c9ea..24d792d9e 100644 --- a/apps/workspace-engine/pkg/secrets/env/provider.go +++ b/apps/workspace-engine/pkg/secrets/env/provider.go @@ -1,49 +1,53 @@ // Package env implements a secrets.Provider that reads from the // workspace-engine process environment. Every workspace using this provider -// must list the permitted env var names explicitly in allowedKeys to prevent +// must list the permitted env var names explicitly in AllowedKeys to prevent // a tenant from reading arbitrary process state. package env import ( "context" + "encoding/json" "fmt" "os" + "slices" "workspace-engine/pkg/secrets" ) const Type = "env" +// Config is the decrypted config payload for an env provider row. +type Config struct { + AllowedKeys []string `json:"allowedKeys"` +} + +func (c Config) validate() error { + if len(c.AllowedKeys) == 0 { + return fmt.Errorf("env provider: allowedKeys is empty") + } + if slices.Contains(c.AllowedKeys, "") { + return fmt.Errorf("env provider: allowedKeys entries must be non-empty strings") + } + return nil +} + type Provider struct { allowed map[string]struct{} lookup func(string) (string, bool) } -// Factory matches secrets.ProviderFactory. The config must contain an -// allowedKeys array with at least one entry; the API and Drizzle validators -// enforce the shape, the factory enforces it again defensively. -func Factory(cfg map[string]any) (secrets.Provider, error) { - raw, ok := cfg["allowedKeys"] - if !ok { - return nil, fmt.Errorf("env provider: missing allowedKeys in config") - } - list, ok := raw.([]any) - if !ok { - return nil, fmt.Errorf("env provider: allowedKeys must be a JSON array, got %T", raw) +// Factory matches secrets.ProviderFactory. +func Factory(raw json.RawMessage) (secrets.Provider, error) { + var cfg Config + if err := json.Unmarshal(raw, &cfg); err != nil { + return nil, fmt.Errorf("env provider: parse config: %w", err) } - if len(list) == 0 { - return nil, fmt.Errorf("env provider: allowedKeys is empty") + if err := cfg.validate(); err != nil { + return nil, err } - allowed := make(map[string]struct{}, len(list)) - for _, v := range list { - s, ok := v.(string) - if !ok || s == "" { - return nil, fmt.Errorf( - "env provider: allowedKeys entries must be non-empty strings, got %T", - v, - ) - } - allowed[s] = struct{}{} + allowed := make(map[string]struct{}, len(cfg.AllowedKeys)) + for _, k := range cfg.AllowedKeys { + allowed[k] = struct{}{} } return &Provider{allowed: allowed, lookup: os.LookupEnv}, nil } diff --git a/apps/workspace-engine/pkg/secrets/env/provider_test.go b/apps/workspace-engine/pkg/secrets/env/provider_test.go index 87f1dff1d..e3a3bc53f 100644 --- a/apps/workspace-engine/pkg/secrets/env/provider_test.go +++ b/apps/workspace-engine/pkg/secrets/env/provider_test.go @@ -2,14 +2,19 @@ package env import ( "context" + "encoding/json" "testing" "workspace-engine/pkg/secrets" ) -func newTestProvider(t *testing.T, cfg map[string]any, envVars map[string]string) *Provider { +func newTestProvider(t *testing.T, cfg Config, envVars map[string]string) *Provider { t.Helper() - p, err := Factory(cfg) + raw, err := json.Marshal(cfg) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + p, err := Factory(raw) if err != nil { t.Fatalf("Factory: %v", err) } @@ -23,7 +28,7 @@ func newTestProvider(t *testing.T, cfg map[string]any, envVars map[string]string func TestResolveHappyPath(t *testing.T) { p := newTestProvider(t, - map[string]any{"allowedKeys": []any{"FOO", "BAR"}}, + Config{AllowedKeys: []string{"FOO", "BAR"}}, map[string]string{"FOO": "value-foo"}, ) got, err := p.Resolve(context.Background(), secrets.SecretReference{Key: "FOO"}) @@ -37,7 +42,7 @@ func TestResolveHappyPath(t *testing.T) { func TestResolveRejectsNotInAllowlist(t *testing.T) { p := newTestProvider(t, - map[string]any{"allowedKeys": []any{"FOO"}}, + Config{AllowedKeys: []string{"FOO"}}, map[string]string{"FOO": "x", "BAR": "y"}, ) if _, err := p.Resolve(context.Background(), secrets.SecretReference{Key: "BAR"}); err == nil { @@ -47,7 +52,7 @@ func TestResolveRejectsNotInAllowlist(t *testing.T) { func TestResolveMissingEnvVar(t *testing.T) { p := newTestProvider(t, - map[string]any{"allowedKeys": []any{"FOO"}}, + Config{AllowedKeys: []string{"FOO"}}, map[string]string{}, ) if _, err := p.Resolve(context.Background(), secrets.SecretReference{Key: "FOO"}); err == nil { @@ -58,17 +63,17 @@ func TestResolveMissingEnvVar(t *testing.T) { func TestFactoryRejectsBadConfigs(t *testing.T) { cases := []struct { name string - cfg map[string]any + raw string }{ - {"missing", map[string]any{}}, - {"wrong type", map[string]any{"allowedKeys": "FOO"}}, - {"empty list", map[string]any{"allowedKeys": []any{}}}, - {"non-string entry", map[string]any{"allowedKeys": []any{"FOO", 42}}}, - {"empty string entry", map[string]any{"allowedKeys": []any{""}}}, + {"not json", `not-json`}, + {"missing", `{}`}, + {"wrong type", `{"allowedKeys":"FOO"}`}, + {"empty list", `{"allowedKeys":[]}`}, + {"empty string entry", `{"allowedKeys":[""]}`}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { - if _, err := Factory(c.cfg); err == nil { + if _, err := Factory([]byte(c.raw)); err == nil { t.Fatal("expected error") } }) diff --git a/apps/workspace-engine/pkg/secrets/registry_test.go b/apps/workspace-engine/pkg/secrets/registry_test.go index b68e61dbc..a28e23f1d 100644 --- a/apps/workspace-engine/pkg/secrets/registry_test.go +++ b/apps/workspace-engine/pkg/secrets/registry_test.go @@ -2,6 +2,7 @@ package secrets import ( "context" + "encoding/json" "errors" "testing" ) @@ -16,10 +17,10 @@ func (s *stubProvider) Resolve(_ context.Context, _ SecretReference) (string, er func TestRegistryBuildAndTypes(t *testing.T) { r := NewRegistry() - r.Register("doppler", func(_ map[string]any) (Provider, error) { + r.Register("doppler", func(_ json.RawMessage) (Provider, error) { return &stubProvider{name: "doppler"}, nil }) - r.Register("aws_secrets_manager", func(_ map[string]any) (Provider, error) { + r.Register("aws_secrets_manager", func(_ json.RawMessage) (Provider, error) { return &stubProvider{name: "aws_secrets_manager"}, nil }) @@ -28,7 +29,7 @@ func TestRegistryBuildAndTypes(t *testing.T) { t.Fatalf("expected 2 registered types, got %d (%v)", len(types), types) } - p, err := r.Build(&ProviderConfig{Type: "doppler", Config: map[string]any{}}) + p, err := r.Build(&ProviderConfig{Type: "doppler", Config: json.RawMessage(`{}`)}) if err != nil { t.Fatalf("Build doppler: %v", err) } @@ -39,7 +40,7 @@ func TestRegistryBuildAndTypes(t *testing.T) { func TestRegistryUnknownTypeFails(t *testing.T) { r := NewRegistry() - _, err := r.Build(&ProviderConfig{Type: "vault", Config: map[string]any{}}) + _, err := r.Build(&ProviderConfig{Type: "vault", Config: json.RawMessage(`{}`)}) if err == nil { t.Fatal("expected error for unregistered type") } @@ -48,10 +49,10 @@ func TestRegistryUnknownTypeFails(t *testing.T) { func TestRegistryFactoryErrorPropagates(t *testing.T) { r := NewRegistry() wantErr := errors.New("bad config") - r.Register("doppler", func(_ map[string]any) (Provider, error) { + r.Register("doppler", func(_ json.RawMessage) (Provider, error) { return nil, wantErr }) - _, err := r.Build(&ProviderConfig{Type: "doppler", Config: map[string]any{}}) + _, err := r.Build(&ProviderConfig{Type: "doppler", Config: json.RawMessage(`{}`)}) if !errors.Is(err, wantErr) { t.Fatalf("expected wrapped wantErr, got %v", err) } diff --git a/apps/workspace-engine/pkg/secrets/resolver_test.go b/apps/workspace-engine/pkg/secrets/resolver_test.go index 492712aa4..a98cc3fe5 100644 --- a/apps/workspace-engine/pkg/secrets/resolver_test.go +++ b/apps/workspace-engine/pkg/secrets/resolver_test.go @@ -2,6 +2,7 @@ package secrets import ( "context" + "encoding/json" "errors" "sync/atomic" "testing" @@ -65,7 +66,10 @@ func (p *mockProvider) Resolve(_ context.Context, ref SecretReference) (string, func newResolver(t *testing.T, store ProviderConfigStore, provider Provider) *Resolver { t.Helper() reg := NewRegistry() - reg.Register(provider.Type(), func(_ map[string]any) (Provider, error) { return provider, nil }) + reg.Register( + provider.Type(), + func(_ json.RawMessage) (Provider, error) { return provider, nil }, + ) return NewResolver(store, reg, NewCache(time.Minute)) } @@ -77,7 +81,7 @@ func TestResolverHappyPath(t *testing.T) { WorkspaceID: ws, Name: "doppler-prod", Type: "doppler", - Config: map[string]any{"serviceToken": "dp.st.test"}, + Config: json.RawMessage(`{"serviceToken":"dp.st.test"}`), }, }, } @@ -108,7 +112,7 @@ func TestResolverCacheHitsSkipStore(t *testing.T) { WorkspaceID: ws, Name: "doppler-prod", Type: "doppler", - Config: map[string]any{}, + Config: json.RawMessage(`{}`), }, }, } @@ -139,7 +143,7 @@ func TestResolverInvalidationForcesRefetch(t *testing.T) { WorkspaceID: ws, Name: "doppler-prod", Type: "doppler", - Config: map[string]any{}, + Config: json.RawMessage(`{}`), }, }, } @@ -187,7 +191,7 @@ func TestResolverNoFactoryRegistered(t *testing.T) { WorkspaceID: ws, Name: "unknown", Type: "vault", - Config: map[string]any{}, + Config: json.RawMessage(`{}`), }, }, } @@ -207,7 +211,7 @@ func TestResolverProviderErrorPropagates(t *testing.T) { WorkspaceID: ws, Name: "doppler-prod", Type: "doppler", - Config: map[string]any{}, + Config: json.RawMessage(`{}`), }, }, } diff --git a/apps/workspace-engine/pkg/secrets/store.go b/apps/workspace-engine/pkg/secrets/store.go index 6a420cda1..a907bc710 100644 --- a/apps/workspace-engine/pkg/secrets/store.go +++ b/apps/workspace-engine/pkg/secrets/store.go @@ -2,7 +2,6 @@ package secrets import ( "context" - "encoding/json" "fmt" "github.com/google/uuid" @@ -86,15 +85,11 @@ func (s *PostgresStore) toProviderConfig(row db.SecretProvider) (*ProviderConfig if err != nil { return nil, fmt.Errorf("secrets: decrypt config for %q: %w", row.Name, err) } - cfg := make(map[string]any) - if err := json.Unmarshal([]byte(plaintext), &cfg); err != nil { - return nil, fmt.Errorf("secrets: parse decrypted config for %q: %w", row.Name, err) - } return &ProviderConfig{ ID: row.ID, WorkspaceID: row.WorkspaceID, Name: row.Name, Type: string(row.Type), - Config: cfg, + Config: []byte(plaintext), }, nil } diff --git a/apps/workspace-engine/pkg/secrets/types.go b/apps/workspace-engine/pkg/secrets/types.go index bcedfe2cf..fd5092a7c 100644 --- a/apps/workspace-engine/pkg/secrets/types.go +++ b/apps/workspace-engine/pkg/secrets/types.go @@ -7,6 +7,7 @@ package secrets import ( "context" + "encoding/json" "github.com/google/uuid" ) @@ -34,18 +35,19 @@ type Provider interface { Resolve(ctx context.Context, ref SecretReference) (string, error) } -// ProviderConfig is the decrypted view of a secret_provider row. +// ProviderConfig is the decrypted view of a secret_provider row. Config is +// the raw decrypted JSON payload; each provider's factory unmarshals it into +// a typed struct that lives next to the provider implementation. type ProviderConfig struct { ID uuid.UUID WorkspaceID uuid.UUID Name string Type string - // Config is the decrypted JSON payload, shape determined by Type. - Config map[string]any + Config json.RawMessage } -// ProviderFactory constructs a Provider from a decrypted config. -type ProviderFactory func(cfg map[string]any) (Provider, error) +// ProviderFactory constructs a Provider from the decrypted config payload. +type ProviderFactory func(cfg json.RawMessage) (Provider, error) // ProviderConfigStore loads and decrypts secret_provider rows. type ProviderConfigStore interface { diff --git a/apps/workspace-engine/svc/controllers/deploymentplan/controller.go b/apps/workspace-engine/svc/controllers/deploymentplan/controller.go index 83594abd8..2841e97c7 100644 --- a/apps/workspace-engine/svc/controllers/deploymentplan/controller.go +++ b/apps/workspace-engine/svc/controllers/deploymentplan/controller.go @@ -20,6 +20,7 @@ import ( "workspace-engine/pkg/reconcile" "workspace-engine/pkg/reconcile/events" "workspace-engine/pkg/reconcile/postgres" + "workspace-engine/pkg/secrets" "workspace-engine/pkg/selector" "workspace-engine/svc" "workspace-engine/svc/controllers/desiredrelease/variableresolver" @@ -252,7 +253,11 @@ func (c *Controller) processTarget( return nil } -func New(workerID string, pgxPool *pgxpool.Pool) svc.Service { +func New( + workerID string, + pgxPool *pgxpool.Pool, + secretResolver *secrets.Resolver, +) svc.Service { if pgxPool == nil { slog.Error("Failed to get pgx pool") os.Exit(1) @@ -281,7 +286,7 @@ func New(workerID string, pgxPool *pgxpool.Pool) svc.Service { controller := &Controller{ getter: &PostgresGetter{}, setter: &PostgresSetter{queue: enqueueQueue}, - varResolver: NewPostgresVarResolver(variableresolver.NewPostgresGetter(q)), + varResolver: NewPostgresVarResolver(variableresolver.NewPostgresGetter(q), secretResolver), } worker, err := reconcile.NewWorker( diff --git a/apps/workspace-engine/svc/controllers/deploymentplan/getters_postgres.go b/apps/workspace-engine/svc/controllers/deploymentplan/getters_postgres.go index 8af0b3274..e740d857d 100644 --- a/apps/workspace-engine/svc/controllers/deploymentplan/getters_postgres.go +++ b/apps/workspace-engine/svc/controllers/deploymentplan/getters_postgres.go @@ -94,11 +94,15 @@ func (g *PostgresGetter) GetWorkspaceByID( } type PostgresVarResolver struct { - getter variableresolver.Getter + getter variableresolver.Getter + secretResolver variableresolver.SecretResolver } -func NewPostgresVarResolver(getter variableresolver.Getter) *PostgresVarResolver { - return &PostgresVarResolver{getter: getter} +func NewPostgresVarResolver( + getter variableresolver.Getter, + secretResolver variableresolver.SecretResolver, +) *PostgresVarResolver { + return &PostgresVarResolver{getter: getter, secretResolver: secretResolver} } func (r *PostgresVarResolver) Resolve( @@ -106,5 +110,12 @@ func (r *PostgresVarResolver) Resolve( scope *variableresolver.Scope, deploymentID, resourceID string, ) (map[string]oapi.LiteralValue, error) { - return variableresolver.Resolve(ctx, r.getter, scope, deploymentID, resourceID) + return variableresolver.Resolve( + ctx, + r.getter, + r.secretResolver, + scope, + deploymentID, + resourceID, + ) } diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/controller.go b/apps/workspace-engine/svc/controllers/desiredrelease/controller.go index da82194df..8b03d4e3e 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/controller.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/controller.go @@ -16,6 +16,7 @@ import ( "workspace-engine/pkg/reconcile" "workspace-engine/pkg/reconcile/events" "workspace-engine/pkg/reconcile/postgres" + "workspace-engine/pkg/secrets" "workspace-engine/pkg/store/policies" "workspace-engine/pkg/store/releasetargets" "workspace-engine/svc" @@ -25,9 +26,10 @@ var tracer = otel.Tracer("workspace-engine/svc/controllers/desiredrelease") var _ reconcile.Processor = (*Controller)(nil) type Controller struct { - getter Getter - queries *db.Queries - setter Setter + getter Getter + queries *db.Queries + setter Setter + secretResolver *secrets.Resolver } // Process implements [reconcile.Processor]. @@ -77,7 +79,7 @@ func (c *Controller) Process(ctx context.Context, item reconcile.Item) (reconcil return reconcile.Result{}, nil } - result, err := Reconcile(ctx, item.WorkspaceID, getter, c.setter, rt) + result, err := Reconcile(ctx, item.WorkspaceID, getter, c.setter, c.secretResolver, rt) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) @@ -96,11 +98,11 @@ func (c *Controller) Process(ctx context.Context, item reconcile.Item) (reconcil // NewController creates a Controller with the given dependencies. // Use this constructor in tests to inject mock implementations. -func NewController(getter Getter, setter Setter) *Controller { - return &Controller{getter: getter, setter: setter} +func NewController(getter Getter, setter Setter, secretResolver *secrets.Resolver) *Controller { + return &Controller{getter: getter, setter: setter, secretResolver: secretResolver} } -func New(workerID string, pgxPool *pgxpool.Pool) svc.Service { +func New(workerID string, pgxPool *pgxpool.Pool, secretResolver *secrets.Resolver) svc.Service { if pgxPool == nil { slog.Error("Failed to get pgx pool") os.Exit(1) @@ -126,8 +128,9 @@ func New(workerID string, pgxPool *pgxpool.Pool) svc.Service { queue := postgres.NewForKinds(pgxPool, kind) enqueueQueue := postgres.New(pgxPool) controller := &Controller{ - queries: db.GetQueries(ctx), - setter: NewPostgresSetter(enqueueQueue), + queries: db.GetQueries(ctx), + setter: NewPostgresSetter(enqueueQueue), + secretResolver: secretResolver, } worker, err := reconcile.NewWorker( kind, diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go b/apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go index 78697f69f..e1d5ae4d3 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go @@ -10,6 +10,7 @@ import ( "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" "workspace-engine/pkg/oapi" + "workspace-engine/pkg/secrets" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector" "workspace-engine/svc/controllers/desiredrelease/policyeval" @@ -26,9 +27,10 @@ type ReconcileResult struct { type reconciler struct { workspaceID uuid.UUID - getter Getter - setter Setter - rt *ReleaseTarget + getter Getter + setter Setter + secretResolver *secrets.Resolver + rt *ReleaseTarget scope *evaluator.EvaluatorScope policies []*oapi.Policy @@ -83,7 +85,7 @@ func (r *reconciler) resolveVariables(ctx context.Context) error { Environment: r.scope.Environment, } vars, err := variableresolver.Resolve( - ctx, r.getter, varScope, + ctx, r.getter, r.secretResolver, varScope, r.rt.DeploymentID.String(), r.rt.ResourceID.String(), ) if err != nil { @@ -113,6 +115,7 @@ func Reconcile( workspaceID string, getter Getter, setter Setter, + secretResolver *secrets.Resolver, rt *ReleaseTarget, ) (*ReconcileResult, error) { ctx, span := tracer.Start(ctx, "desiredrelease.Reconcile") @@ -124,7 +127,13 @@ func Reconcile( if err != nil { return nil, fmt.Errorf("parse workspace id: %w", err) } - r := &reconciler{workspaceID: workspaceIDUUID, getter: getter, setter: setter, rt: rt} + r := &reconciler{ + workspaceID: workspaceIDUUID, + getter: getter, + setter: setter, + secretResolver: secretResolver, + rt: rt, + } r.rt.WorkspaceID = r.workspaceID if err := r.loadInput(ctx); err != nil { diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go b/apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go index a00c1f460..ed28629e7 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go @@ -338,7 +338,7 @@ func TestReconcile_NoVersions(t *testing.T) { } setter := &mockReconcileSetter{} - result, err := Reconcile(ctx, rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(ctx, rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) assert.NotNil(t, result) assert.Nil(t, result.NextReconcileAt) @@ -358,7 +358,7 @@ func TestReconcile_AllPoliciesAllow(t *testing.T) { } setter := &mockReconcileSetter{} - result, err := Reconcile(ctx, rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(ctx, rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) assert.NotNil(t, result) assert.Nil(t, result.NextReconcileAt) @@ -394,7 +394,7 @@ func TestReconcile_PolicyDeniesAllVersions(t *testing.T) { } setter := &mockReconcileSetter{} - result, err := Reconcile(ctx, rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(ctx, rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) assert.NotNil(t, result) @@ -420,7 +420,7 @@ func TestReconcile_SelectsFirstPassingVersion(t *testing.T) { } setter := &mockReconcileSetter{} - result, err := Reconcile(ctx, rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(ctx, rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) assert.NotNil(t, result) @@ -460,7 +460,7 @@ func TestReconcile_AllVersionsDenied_NoRelease(t *testing.T) { } setter := &mockReconcileSetter{} - result, err := Reconcile(ctx, rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(ctx, rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) assert.Nil(t, result.NextReconcileAt) assert.Empty(t, setter.releases, "no version should pass the approval gate") @@ -479,7 +479,7 @@ func TestReconcile_UpsertsEvaluationsForPassingVersion(t *testing.T) { } setter := &mockReconcileSetter{} - _, err := Reconcile(ctx, rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(ctx, rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.releases, 1, "version should pass with no policies") diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go index ab5a5290c..5e3ebe2a0 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go @@ -45,6 +45,7 @@ type Scope struct { func Resolve( ctx context.Context, getter Getter, + secretResolver SecretResolver, scope *Scope, deploymentID, resourceID string, ) (map[string]oapi.LiteralValue, error) { @@ -112,6 +113,8 @@ func Resolve( if lv := resolveFromResource( ctx, resolver, + secretResolver, + wsID, resourceID, key, resourceVars, @@ -126,6 +129,8 @@ func Resolve( if lv := resolveFromValues( ctx, resolver, + secretResolver, + wsID, resourceID, dv.Values, scope.Resource, @@ -141,6 +146,8 @@ func Resolve( key, filteredVariableSets, resolver, + secretResolver, + wsID, resourceID, entity, ); lv != nil { @@ -248,6 +255,8 @@ func (r *realtimeResolver) ResolveRelated( func resolveFromResource( ctx context.Context, resolver RelatedEntityResolver, + secretResolver SecretResolver, + workspaceID uuid.UUID, resourceID string, key string, resourceVars map[string][]oapi.ResourceVariable, @@ -278,7 +287,15 @@ func resolveFromResource( }) for _, rv := range matched { - lv, err := ResolveValue(ctx, resolver, resourceID, entity, &rv.Value) + lv, err := ResolveValue( + ctx, + resolver, + secretResolver, + workspaceID, + resourceID, + entity, + &rv.Value, + ) if err == nil && lv != nil { return lv } @@ -291,6 +308,8 @@ func resolveFromResource( func resolveFromValues( ctx context.Context, resolver RelatedEntityResolver, + secretResolver SecretResolver, + workspaceID uuid.UUID, resourceID string, values []oapi.DeploymentVariableValue, resource *oapi.Resource, @@ -316,7 +335,15 @@ func resolveFromValues( }) for _, v := range matched { - lv, err := ResolveValue(ctx, resolver, resourceID, entity, &v.Value) + lv, err := ResolveValue( + ctx, + resolver, + secretResolver, + workspaceID, + resourceID, + entity, + &v.Value, + ) if err == nil && lv != nil { return lv } @@ -329,13 +356,23 @@ func resolveFromVariableSets( key string, variableSets []oapi.VariableSetWithVariables, resolver RelatedEntityResolver, + secretResolver SecretResolver, + workspaceID uuid.UUID, resourceID string, entity *oapi.RelatableEntity, ) *oapi.LiteralValue { for _, vs := range variableSets { for _, v := range vs.Variables { if v.Key == key { - lv, err := ResolveValue(ctx, resolver, resourceID, entity, &v.Value) + lv, err := ResolveValue( + ctx, + resolver, + secretResolver, + workspaceID, + resourceID, + entity, + &v.Value, + ) if err == nil && lv != nil { return lv } diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go index 4241c6e2a..d7392e731 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go @@ -196,7 +196,15 @@ func TestResolveValue_Literal_String(t *testing.T) { scope := newScope() val := literalStringValue("hello") entity := makeResourceEntity(scope.Resource) - lv, err := ResolveValue(context.Background(), emptyResolver, scope.Resource.Id, &entity, &val) + lv, err := ResolveValue( + context.Background(), + emptyResolver, + nil, + uuid.Nil, + scope.Resource.Id, + &entity, + &val, + ) require.NoError(t, err) s, err := lv.AsStringValue() require.NoError(t, err) @@ -207,7 +215,15 @@ func TestResolveValue_Literal_Int(t *testing.T) { scope := newScope() val := literalIntValue(42) entity := makeResourceEntity(scope.Resource) - lv, err := ResolveValue(context.Background(), emptyResolver, scope.Resource.Id, &entity, &val) + lv, err := ResolveValue( + context.Background(), + emptyResolver, + nil, + uuid.Nil, + scope.Resource.Id, + &entity, + &val, + ) require.NoError(t, err) i, err := lv.AsIntegerValue() require.NoError(t, err) @@ -218,7 +234,15 @@ func TestResolveValue_Literal_Bool(t *testing.T) { scope := newScope() val := literalBoolValue(true) entity := makeResourceEntity(scope.Resource) - lv, err := ResolveValue(context.Background(), emptyResolver, scope.Resource.Id, &entity, &val) + lv, err := ResolveValue( + context.Background(), + emptyResolver, + nil, + uuid.Nil, + scope.Resource.Id, + &entity, + &val, + ) require.NoError(t, err) b, err := lv.AsBooleanValue() require.NoError(t, err) @@ -250,7 +274,15 @@ func TestResolveValue_Reference_ResourceName(t *testing.T) { } val := referenceValue("database", "name") - lv, err := ResolveValue(context.Background(), resolver, scope.Resource.Id, &entity, &val) + lv, err := ResolveValue( + context.Background(), + resolver, + nil, + uuid.Nil, + scope.Resource.Id, + &entity, + &val, + ) require.NoError(t, err) s, err := lv.AsStringValue() require.NoError(t, err) @@ -278,7 +310,15 @@ func TestResolveValue_Reference_ResourceMetadata(t *testing.T) { } val := referenceValue("network", "metadata", "cidr") - lv, err := ResolveValue(context.Background(), resolver, scope.Resource.Id, &entity, &val) + lv, err := ResolveValue( + context.Background(), + resolver, + nil, + uuid.Nil, + scope.Resource.Id, + &entity, + &val, + ) require.NoError(t, err) s, err := lv.AsStringValue() require.NoError(t, err) @@ -301,7 +341,15 @@ func TestResolveValue_Reference_DeploymentName(t *testing.T) { } val := referenceValue("parent-deployment", "name") - lv, err := ResolveValue(context.Background(), resolver, scope.Resource.Id, &entity, &val) + lv, err := ResolveValue( + context.Background(), + resolver, + nil, + uuid.Nil, + scope.Resource.Id, + &entity, + &val, + ) require.NoError(t, err) s, err := lv.AsStringValue() require.NoError(t, err) @@ -323,7 +371,15 @@ func TestResolveValue_Reference_EnvironmentName(t *testing.T) { } val := referenceValue("env", "name") - lv, err := ResolveValue(context.Background(), resolver, scope.Resource.Id, &entity, &val) + lv, err := ResolveValue( + context.Background(), + resolver, + nil, + uuid.Nil, + scope.Resource.Id, + &entity, + &val, + ) require.NoError(t, err) s, err := lv.AsStringValue() require.NoError(t, err) @@ -334,7 +390,15 @@ func TestResolveValue_Reference_NotFound(t *testing.T) { scope := newScope() entity := makeResourceEntity(scope.Resource) val := referenceValue("nonexistent", "name") - _, err := ResolveValue(context.Background(), emptyResolver, scope.Resource.Id, &entity, &val) + _, err := ResolveValue( + context.Background(), + emptyResolver, + nil, + uuid.Nil, + scope.Resource.Id, + &entity, + &val, + ) require.Error(t, err) assert.Contains(t, err.Error(), "not found") } @@ -360,7 +424,15 @@ func TestResolveValue_Reference_BadPath(t *testing.T) { } val := referenceValue("database", "metadata", "missing_key") - _, err := ResolveValue(context.Background(), resolver, scope.Resource.Id, &entity, &val) + _, err := ResolveValue( + context.Background(), + resolver, + nil, + uuid.Nil, + scope.Resource.Id, + &entity, + &val, + ) require.Error(t, err) assert.Contains(t, err.Error(), "not found") } @@ -412,6 +484,7 @@ func TestResolve_ResourceVariableSelectorPriority(t *testing.T) { resolved, err := Resolve( context.Background(), getter, + nil, scope, scope.Deployment.Id, scope.Resource.Id, @@ -453,6 +526,7 @@ func TestResolve_ResourceVarWins(t *testing.T) { resolved, err := Resolve( context.Background(), getter, + nil, scope, scope.Deployment.Id, scope.Resource.Id, @@ -492,6 +566,7 @@ func TestResolve_DeploymentVariableValueUsedWhenNoResourceVar(t *testing.T) { resolved, err := Resolve( context.Background(), getter, + nil, scope, scope.Deployment.Id, scope.Resource.Id, @@ -531,6 +606,7 @@ func TestResolve_DefaultValueFallback(t *testing.T) { resolved, err := Resolve( context.Background(), getter, + nil, scope, scope.Deployment.Id, scope.Resource.Id, @@ -564,6 +640,7 @@ func TestResolve_NoMatchNoDefault_KeyAbsent(t *testing.T) { resolved, err := Resolve( context.Background(), getter, + nil, scope, scope.Deployment.Id, scope.Resource.Id, @@ -614,6 +691,7 @@ func TestResolve_HighestPriorityValueWins(t *testing.T) { resolved, err := Resolve( context.Background(), getter, + nil, scope, scope.Deployment.Id, scope.Resource.Id, @@ -815,6 +893,7 @@ func TestResolve_DeploymentVarValue_DefaultAndSelectorGatedOverride(t *testing.T resolved, err := Resolve( context.Background(), getter, + nil, scope, scope.Deployment.Id, scope.Resource.Id, @@ -847,6 +926,7 @@ func TestResolve_MultipleVariables(t *testing.T) { resolved, err := Resolve( context.Background(), getter, + nil, scope, scope.Deployment.Id, scope.Resource.Id, @@ -874,6 +954,7 @@ func TestResolve_NoDeploymentVars_EmptyMap(t *testing.T) { resolved, err := Resolve( context.Background(), emptyGetter, + nil, scope, scope.Deployment.Id, scope.Resource.Id, @@ -955,6 +1036,7 @@ func TestResolve_ResourceVar_WithReference(t *testing.T) { resolved, err := Resolve( context.Background(), getter, + nil, scope, scope.Deployment.Id, scope.Resource.Id, @@ -1036,6 +1118,7 @@ func TestResolve_DeploymentVarValue_WithReference(t *testing.T) { resolved, err := Resolve( context.Background(), getter, + nil, scope, scope.Deployment.Id, scope.Resource.Id, @@ -1133,6 +1216,7 @@ func TestResolve_MixedLiteralAndReference(t *testing.T) { resolved, err := Resolve( context.Background(), getter, + nil, scope, scope.Deployment.Id, scope.Resource.Id, @@ -1182,6 +1266,7 @@ func TestResolve_ResourceVarRefFails_FallsToDeploymentValue(t *testing.T) { resolved, err := Resolve( context.Background(), getter, + nil, scope, scope.Deployment.Id, scope.Resource.Id, @@ -1213,7 +1298,15 @@ func TestResolveValue_Reference_ResourceConfig(t *testing.T) { } val := referenceValue("self", "config", "networking", "vpc_id") - lv, err := ResolveValue(context.Background(), resolver, scope.Resource.Id, &entity, &val) + lv, err := ResolveValue( + context.Background(), + resolver, + nil, + uuid.Nil, + scope.Resource.Id, + &entity, + &val, + ) require.NoError(t, err) s, err := lv.AsStringValue() require.NoError(t, err) @@ -1230,7 +1323,15 @@ func TestResolveValue_Sensitive_ReturnsError(t *testing.T) { _ = v.FromSensitiveValue(oapi.SensitiveValue{ValueHash: "abc123"}) entity := makeResourceEntity(scope.Resource) - _, err := ResolveValue(context.Background(), emptyResolver, scope.Resource.Id, &entity, v) + _, err := ResolveValue( + context.Background(), + emptyResolver, + nil, + uuid.Nil, + scope.Resource.Id, + &entity, + v, + ) require.Error(t, err) assert.Contains(t, err.Error(), "sensitive") } @@ -1296,6 +1397,7 @@ func TestResolve_VariableSet_SimpleInjection(t *testing.T) { resolved, err := Resolve( context.Background(), getter, + nil, scope, scope.Deployment.Id, scope.Resource.Id, @@ -1346,6 +1448,7 @@ func TestResolve_VariableSet_DoesNotOverwriteResourceVar(t *testing.T) { resolved, err := Resolve( context.Background(), getter, + nil, scope, scope.Deployment.Id, scope.Resource.Id, @@ -1396,6 +1499,7 @@ func TestResolve_VariableSet_DoesNotOverwriteDeploymentVarValue(t *testing.T) { resolved, err := Resolve( context.Background(), getter, + nil, scope, scope.Deployment.Id, scope.Resource.Id, @@ -1456,6 +1560,7 @@ func TestResolve_VariableSet_HighestPriorityWins(t *testing.T) { resolved, err := Resolve( context.Background(), getter, + nil, scope, scope.Deployment.Id, scope.Resource.Id, @@ -1500,6 +1605,7 @@ func TestResolve_VariableSet_UnrelatedDoNotMatch(t *testing.T) { resolved, err := Resolve( context.Background(), getter, + nil, scope, scope.Deployment.Id, scope.Resource.Id, diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/value.go b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/value.go index 53b444f26..6ff2803fd 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/value.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/value.go @@ -4,7 +4,9 @@ import ( "context" "fmt" + "github.com/google/uuid" "workspace-engine/pkg/oapi" + "workspace-engine/pkg/secrets" "workspace-engine/pkg/workspace/relationships" ) @@ -15,20 +17,29 @@ type RelatedEntityResolver interface { ResolveRelated(ctx context.Context, reference string) ([]*oapi.RelatableEntity, error) } +// SecretResolver fetches the plaintext value for a SecretReferenceValue. +// *secrets.Resolver satisfies this interface; tests use fakes. +type SecretResolver interface { + Resolve(ctx context.Context, workspaceID uuid.UUID, ref secrets.SecretReference) (string, error) +} + // ResolveValue resolves a single oapi.Value to a concrete LiteralValue. // // Literal values are returned as-is. Reference values are resolved by // finding related entities through the resolver and traversing the property -// path on the matched entity. Sensitive values are not resolved and return -// an error — they must be handled by a separate decryption path. +// path on the matched entity. Secret references are fetched through the +// SecretResolver and returned as string literals. Sensitive values without a +// concrete provider reference remain unresolvable. func ResolveValue( ctx context.Context, resolver RelatedEntityResolver, + secretResolver SecretResolver, + workspaceID uuid.UUID, resourceID string, entity *oapi.RelatableEntity, value *oapi.Value, ) (*oapi.LiteralValue, error) { - _, span := tracer.Start(ctx, "variableresolver.ResolveValue") + ctx, span := tracer.Start(ctx, "variableresolver.ResolveValue") defer span.End() valueType, err := value.GetType() @@ -41,6 +52,8 @@ func ResolveValue( return resolveLiteral(value) case "reference": return resolveReference(ctx, resolver, value, entity) + case "secret_ref": + return resolveSecretReference(ctx, secretResolver, workspaceID, value) case "sensitive": return nil, fmt.Errorf("sensitive values are not resolved by the variable resolver") default: @@ -48,6 +61,45 @@ func ResolveValue( } } +func resolveSecretReference( + ctx context.Context, + secretResolver SecretResolver, + workspaceID uuid.UUID, + value *oapi.Value, +) (*oapi.LiteralValue, error) { + if secretResolver == nil { + return nil, fmt.Errorf("secret_ref encountered but no SecretResolver configured") + } + srv, err := value.AsSecretReferenceValue() + if err != nil { + return nil, fmt.Errorf("extract secret reference value: %w", err) + } + ref := secrets.SecretReference{ + Provider: srv.SecretProvider, + Key: srv.SecretKey, + } + if srv.SecretPath != nil && len(*srv.SecretPath) > 0 { + // Provider-specific path serialization: join with "/" so Doppler + // (project/config) and AWS (secret name + optional segments) can + // reuse the canonical Path field rather than carrying an array. + ref.Path = (*srv.SecretPath)[0] + for i := 1; i < len(*srv.SecretPath); i++ { + ref.Path += "/" + (*srv.SecretPath)[i] + } + } + plaintext, err := secretResolver.Resolve(ctx, workspaceID, ref) + if err != nil { + return nil, fmt.Errorf( + "resolve secret %s/%s/%s: %w", + ref.Provider, + ref.Path, + ref.Key, + err, + ) + } + return oapi.NewLiteralValue(plaintext), nil +} + func resolveLiteral(value *oapi.Value) (*oapi.LiteralValue, error) { lv, err := value.AsLiteralValue() if err != nil { diff --git a/apps/workspace-engine/test/controllers/harness/pipeline.go b/apps/workspace-engine/test/controllers/harness/pipeline.go index bd63e027d..861325c13 100644 --- a/apps/workspace-engine/test/controllers/harness/pipeline.go +++ b/apps/workspace-engine/test/controllers/harness/pipeline.go @@ -150,7 +150,7 @@ func NewTestPipeline(t *testing.T, opts ...PipelineOption) *TestPipeline { releaseSetter.Agents = sc.JobAgents selectorCtrl := selectoreval.NewController(selectorGetter, selectorSetter, qs.shared) - releaseCtrl := desiredrelease.NewController(releaseGetter, releaseSetter) + releaseCtrl := desiredrelease.NewController(releaseGetter, releaseSetter, nil) sel := "true" jobDispatchGetter := &JobDispatchGetter{ From 6286cf0226e8a59051afa278ca8e078de6e16697 Mon Sep 17 00:00:00 2001 From: Michael Leone Date: Mon, 11 May 2026 14:42:03 -0400 Subject: [PATCH 06/13] feat(secrets): populate release.EncryptedVariables for secret_ref values - variableresolver.ResolveValue now returns (*LiteralValue, bool, error); the bool is true only when the resolved value originated from a secret_ref (literal, reference, and the dead "sensitive" path stay false). - The three priority helpers (resolveFromResource, resolveFromValues, resolveFromVariableSets) propagate that flag. variableresolver.Resolve aggregates the keys whose value came from a secret_ref and returns them alongside the resolved map: (map[string]LiteralValue, []string, error). A "resolved.sensitive" OTEL span attribute records the count. - desiredrelease.reconciler captures the slice into sensitiveVars and passes it to buildRelease, which now stores it on release.EncryptedVariables (nil collapses to []string{} for stable JSON). - deploymentplan.VarResolver interface gains the []string return. PostgresVarResolver and the mock used in controller_test.go are updated; deploymentplan.Controller now writes sensitiveKeys directly onto the release it constructs instead of the hardcoded empty slice. - variableresolver test call sites updated to discard the new sensitive return where the test isn't exercising it. --- .../controllers/deploymentplan/controller.go | 7 +- .../deploymentplan/controller_test.go | 8 +-- .../svc/controllers/deploymentplan/getters.go | 6 +- .../deploymentplan/getters_postgres.go | 2 +- .../controllers/desiredrelease/adapters.go | 6 +- .../controllers/desiredrelease/reconcile.go | 14 ++-- .../variableresolver/resolve.go | 67 +++++++++++-------- .../variableresolver/resolve_test.go | 58 ++++++++-------- .../desiredrelease/variableresolver/value.go | 23 +++++-- 9 files changed, 112 insertions(+), 79 deletions(-) diff --git a/apps/workspace-engine/svc/controllers/deploymentplan/controller.go b/apps/workspace-engine/svc/controllers/deploymentplan/controller.go index 2841e97c7..0b377355d 100644 --- a/apps/workspace-engine/svc/controllers/deploymentplan/controller.go +++ b/apps/workspace-engine/svc/controllers/deploymentplan/controller.go @@ -192,13 +192,16 @@ func (c *Controller) processTarget( Deployment: deployment, Environment: env, } - variables, err := c.varResolver.Resolve( + variables, sensitiveKeys, err := c.varResolver.Resolve( ctx, scope, plan.DeploymentID.String(), target.ResourceID.String(), ) if err != nil { return fmt.Errorf("resolve variables: %w", err) } + if sensitiveKeys == nil { + sensitiveKeys = []string{} + } release := &oapi.Release{ CreatedAt: plan.CreatedAt.Time.Format(time.RFC3339), @@ -210,7 +213,7 @@ func (c *Controller) processTarget( }, Variables: variables, Version: *version, - EncryptedVariables: []string{}, + EncryptedVariables: sensitiveKeys, } for i := range matchedAgents { diff --git a/apps/workspace-engine/svc/controllers/deploymentplan/controller_test.go b/apps/workspace-engine/svc/controllers/deploymentplan/controller_test.go index f57d7825d..58922b080 100644 --- a/apps/workspace-engine/svc/controllers/deploymentplan/controller_test.go +++ b/apps/workspace-engine/svc/controllers/deploymentplan/controller_test.go @@ -174,14 +174,14 @@ func (m *mockVarResolver) Resolve( _ context.Context, _ *variableresolver.Scope, _, _ string, -) (map[string]oapi.LiteralValue, error) { +) (map[string]oapi.LiteralValue, []string, error) { if m.err != nil { - return nil, m.err + return nil, nil, m.err } if m.variables == nil { - return map[string]oapi.LiteralValue{}, nil + return map[string]oapi.LiteralValue{}, nil, nil } - return m.variables, nil + return m.variables, nil, nil } // --- helpers --- diff --git a/apps/workspace-engine/svc/controllers/deploymentplan/getters.go b/apps/workspace-engine/svc/controllers/deploymentplan/getters.go index 7608c386c..9ba478507 100644 --- a/apps/workspace-engine/svc/controllers/deploymentplan/getters.go +++ b/apps/workspace-engine/svc/controllers/deploymentplan/getters.go @@ -27,11 +27,13 @@ type Getter interface { GetWorkspaceByID(ctx context.Context, id uuid.UUID) (db.Workspace, error) } -// VarResolver resolves deployment variables for a release target. +// VarResolver resolves deployment variables for a release target. The second +// return is the list of variable keys whose value originated from a +// secret_ref — used to populate release.EncryptedVariables. type VarResolver interface { Resolve( ctx context.Context, scope *variableresolver.Scope, deploymentID, resourceID string, - ) (map[string]oapi.LiteralValue, error) + ) (map[string]oapi.LiteralValue, []string, error) } diff --git a/apps/workspace-engine/svc/controllers/deploymentplan/getters_postgres.go b/apps/workspace-engine/svc/controllers/deploymentplan/getters_postgres.go index e740d857d..20d31a046 100644 --- a/apps/workspace-engine/svc/controllers/deploymentplan/getters_postgres.go +++ b/apps/workspace-engine/svc/controllers/deploymentplan/getters_postgres.go @@ -109,7 +109,7 @@ func (r *PostgresVarResolver) Resolve( ctx context.Context, scope *variableresolver.Scope, deploymentID, resourceID string, -) (map[string]oapi.LiteralValue, error) { +) (map[string]oapi.LiteralValue, []string, error) { return variableresolver.Resolve( ctx, r.getter, diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/adapters.go b/apps/workspace-engine/svc/controllers/desiredrelease/adapters.go index f690a523e..90ee39755 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/adapters.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/adapters.go @@ -10,7 +10,11 @@ func buildRelease( rt *ReleaseTarget, version *oapi.DeploymentVersion, variables map[string]oapi.LiteralValue, + sensitiveKeys []string, ) *oapi.Release { + if sensitiveKeys == nil { + sensitiveKeys = []string{} + } return &oapi.Release{ ReleaseTarget: oapi.ReleaseTarget{ DeploymentId: rt.DeploymentID.String(), @@ -19,7 +23,7 @@ func buildRelease( }, Version: *version, Variables: variables, - EncryptedVariables: []string{}, + EncryptedVariables: sensitiveKeys, CreatedAt: time.Now().Format(time.RFC3339), } } diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go b/apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go index e1d5ae4d3..0fcad4e52 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go @@ -32,10 +32,11 @@ type reconciler struct { secretResolver *secrets.Resolver rt *ReleaseTarget - scope *evaluator.EvaluatorScope - policies []*oapi.Policy - version *oapi.DeploymentVersion - vars map[string]oapi.LiteralValue + scope *evaluator.EvaluatorScope + policies []*oapi.Policy + version *oapi.DeploymentVersion + vars map[string]oapi.LiteralValue + sensitiveVars []string } func (r *reconciler) loadInput(ctx context.Context) (err error) { @@ -84,7 +85,7 @@ func (r *reconciler) resolveVariables(ctx context.Context) error { Deployment: r.scope.Deployment, Environment: r.scope.Environment, } - vars, err := variableresolver.Resolve( + vars, sensitive, err := variableresolver.Resolve( ctx, r.getter, r.secretResolver, varScope, r.rt.DeploymentID.String(), r.rt.ResourceID.String(), ) @@ -92,6 +93,7 @@ func (r *reconciler) resolveVariables(ctx context.Context) error { return err } r.vars = vars + r.sensitiveVars = sensitive return nil } @@ -100,7 +102,7 @@ func (r *reconciler) persistNoDesiredRelease(ctx context.Context) error { } func (r *reconciler) persistRelease(ctx context.Context) (*oapi.Release, error) { - release := buildRelease(r.rt, r.version, r.vars) + release := buildRelease(r.rt, r.version, r.vars, r.sensitiveVars) if err := r.setter.SetDesiredRelease(ctx, r.rt, release); err != nil { return nil, err } diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go index 5e3ebe2a0..f58eb8415 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go @@ -33,7 +33,9 @@ type Scope struct { Environment *oapi.Environment } -// Resolve computes the final set of variables for a release target. +// Resolve computes the final set of variables for a release target. The +// second return is the list of variable keys whose value originated from a +// secret_ref — used to populate release.EncryptedVariables. // // Resolution priority (per variable key): // 1. Resource variable with matching key (highest priority) @@ -48,7 +50,7 @@ func Resolve( secretResolver SecretResolver, scope *Scope, deploymentID, resourceID string, -) (map[string]oapi.LiteralValue, error) { +) (map[string]oapi.LiteralValue, []string, error) { ctx, span := tracer.Start(ctx, "variableresolver.Resolve") defer span.End() @@ -61,32 +63,32 @@ func Resolve( if err != nil { span.RecordError(err) span.SetStatus(codes.Error, "get deployment variables failed") - return nil, fmt.Errorf("get deployment variables: %w", err) + return nil, nil, fmt.Errorf("get deployment variables: %w", err) } span.SetAttributes(attribute.Int("deployment_variables.count", len(deploymentVars))) if len(deploymentVars) == 0 { - return map[string]oapi.LiteralValue{}, nil + return map[string]oapi.LiteralValue{}, nil, nil } resourceVars, err := getter.GetResourceVariables(ctx, resourceID) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, "get resource variables failed") - return nil, fmt.Errorf("get resource variables: %w", err) + return nil, nil, fmt.Errorf("get resource variables: %w", err) } span.SetAttributes(attribute.Int("resource_variables.count", len(resourceVars))) wsID, err := uuid.Parse(scope.Resource.WorkspaceId) if err != nil { - return nil, fmt.Errorf("parse workspace id: %w", err) + return nil, nil, fmt.Errorf("parse workspace id: %w", err) } variableSets, err := getter.GetVariableSetsWithVariables(ctx, wsID) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, "get variable sets with variables failed") - return nil, fmt.Errorf("get variable sets with variables: %w", err) + return nil, nil, fmt.Errorf("get variable sets with variables: %w", err) } span.SetAttributes(attribute.Int("variable_sets.count", len(variableSets))) @@ -97,7 +99,7 @@ func Resolve( if err != nil { span.RecordError(err) span.SetStatus(codes.Error, "get relationship rules failed") - return nil, fmt.Errorf("get relationship rules: %w", err) + return nil, nil, fmt.Errorf("get relationship rules: %w", err) } resolver := newRealtimeResolver(getter, scope.Resource, wsID, rules) @@ -105,12 +107,13 @@ func Resolve( entity := NewResourceEntity(scope.Resource) resolved := make(map[string]oapi.LiteralValue, len(deploymentVars)) + var sensitiveKeys []string var fromResource, fromValue, fromVariableSet int for _, dv := range deploymentVars { key := dv.Variable.Key - if lv := resolveFromResource( + if lv, sensitive := resolveFromResource( ctx, resolver, secretResolver, @@ -122,11 +125,14 @@ func Resolve( entity, ); lv != nil { resolved[key] = *lv + if sensitive { + sensitiveKeys = append(sensitiveKeys, key) + } fromResource++ continue } - if lv := resolveFromValues( + if lv, sensitive := resolveFromValues( ctx, resolver, secretResolver, @@ -137,11 +143,14 @@ func Resolve( entity, ); lv != nil { resolved[key] = *lv + if sensitive { + sensitiveKeys = append(sensitiveKeys, key) + } fromValue++ continue } - if lv := resolveFromVariableSets( + if lv, sensitive := resolveFromVariableSets( ctx, key, filteredVariableSets, @@ -152,6 +161,9 @@ func Resolve( entity, ); lv != nil { resolved[key] = *lv + if sensitive { + sensitiveKeys = append(sensitiveKeys, key) + } fromVariableSet++ continue } @@ -163,8 +175,9 @@ func Resolve( attribute.Int("resolved.from_resource", fromResource), attribute.Int("resolved.from_value", fromValue), attribute.Int("resolved.from_variable_set", fromVariableSet), + attribute.Int("resolved.sensitive", len(sensitiveKeys)), ) - return resolved, nil + return resolved, sensitiveKeys, nil } // realtimeResolver evaluates relationship rules in realtime to resolve @@ -262,10 +275,10 @@ func resolveFromResource( resourceVars map[string][]oapi.ResourceVariable, resource *oapi.Resource, entity *oapi.RelatableEntity, -) *oapi.LiteralValue { +) (*oapi.LiteralValue, bool) { candidates, ok := resourceVars[key] if !ok || len(candidates) == 0 { - return nil + return nil, false } matched := make([]oapi.ResourceVariable, 0, len(candidates)) @@ -279,7 +292,7 @@ func resolveFromResource( } } if len(matched) == 0 { - return nil + return nil, false } sort.Slice(matched, func(i, j int) bool { @@ -287,7 +300,7 @@ func resolveFromResource( }) for _, rv := range matched { - lv, err := ResolveValue( + lv, sensitive, err := ResolveValue( ctx, resolver, secretResolver, @@ -297,10 +310,10 @@ func resolveFromResource( &rv.Value, ) if err == nil && lv != nil { - return lv + return lv, sensitive } } - return nil + return nil, false } // resolveFromValues finds the highest-priority deployment variable value @@ -314,7 +327,7 @@ func resolveFromValues( values []oapi.DeploymentVariableValue, resource *oapi.Resource, entity *oapi.RelatableEntity, -) *oapi.LiteralValue { +) (*oapi.LiteralValue, bool) { matched := make([]oapi.DeploymentVariableValue, 0, len(values)) for _, v := range values { if v.ResourceSelector == nil { @@ -327,7 +340,7 @@ func resolveFromValues( } } if len(matched) == 0 { - return nil + return nil, false } sort.Slice(matched, func(i, j int) bool { @@ -335,7 +348,7 @@ func resolveFromValues( }) for _, v := range matched { - lv, err := ResolveValue( + lv, sensitive, err := ResolveValue( ctx, resolver, secretResolver, @@ -345,10 +358,10 @@ func resolveFromValues( &v.Value, ) if err == nil && lv != nil { - return lv + return lv, sensitive } } - return nil + return nil, false } func resolveFromVariableSets( @@ -360,11 +373,11 @@ func resolveFromVariableSets( workspaceID uuid.UUID, resourceID string, entity *oapi.RelatableEntity, -) *oapi.LiteralValue { +) (*oapi.LiteralValue, bool) { for _, vs := range variableSets { for _, v := range vs.Variables { if v.Key == key { - lv, err := ResolveValue( + lv, sensitive, err := ResolveValue( ctx, resolver, secretResolver, @@ -374,12 +387,12 @@ func resolveFromVariableSets( &v.Value, ) if err == nil && lv != nil { - return lv + return lv, sensitive } } } } - return nil + return nil, false } func filterVariableSets( diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go index d7392e731..59031c068 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go @@ -196,7 +196,7 @@ func TestResolveValue_Literal_String(t *testing.T) { scope := newScope() val := literalStringValue("hello") entity := makeResourceEntity(scope.Resource) - lv, err := ResolveValue( + lv, _, err := ResolveValue( context.Background(), emptyResolver, nil, @@ -215,7 +215,7 @@ func TestResolveValue_Literal_Int(t *testing.T) { scope := newScope() val := literalIntValue(42) entity := makeResourceEntity(scope.Resource) - lv, err := ResolveValue( + lv, _, err := ResolveValue( context.Background(), emptyResolver, nil, @@ -234,7 +234,7 @@ func TestResolveValue_Literal_Bool(t *testing.T) { scope := newScope() val := literalBoolValue(true) entity := makeResourceEntity(scope.Resource) - lv, err := ResolveValue( + lv, _, err := ResolveValue( context.Background(), emptyResolver, nil, @@ -274,7 +274,7 @@ func TestResolveValue_Reference_ResourceName(t *testing.T) { } val := referenceValue("database", "name") - lv, err := ResolveValue( + lv, _, err := ResolveValue( context.Background(), resolver, nil, @@ -310,7 +310,7 @@ func TestResolveValue_Reference_ResourceMetadata(t *testing.T) { } val := referenceValue("network", "metadata", "cidr") - lv, err := ResolveValue( + lv, _, err := ResolveValue( context.Background(), resolver, nil, @@ -341,7 +341,7 @@ func TestResolveValue_Reference_DeploymentName(t *testing.T) { } val := referenceValue("parent-deployment", "name") - lv, err := ResolveValue( + lv, _, err := ResolveValue( context.Background(), resolver, nil, @@ -371,7 +371,7 @@ func TestResolveValue_Reference_EnvironmentName(t *testing.T) { } val := referenceValue("env", "name") - lv, err := ResolveValue( + lv, _, err := ResolveValue( context.Background(), resolver, nil, @@ -390,7 +390,7 @@ func TestResolveValue_Reference_NotFound(t *testing.T) { scope := newScope() entity := makeResourceEntity(scope.Resource) val := referenceValue("nonexistent", "name") - _, err := ResolveValue( + _, _, err := ResolveValue( context.Background(), emptyResolver, nil, @@ -424,7 +424,7 @@ func TestResolveValue_Reference_BadPath(t *testing.T) { } val := referenceValue("database", "metadata", "missing_key") - _, err := ResolveValue( + _, _, err := ResolveValue( context.Background(), resolver, nil, @@ -481,7 +481,7 @@ func TestResolve_ResourceVariableSelectorPriority(t *testing.T) { }, } - resolved, err := Resolve( + resolved, _, err := Resolve( context.Background(), getter, nil, @@ -523,7 +523,7 @@ func TestResolve_ResourceVarWins(t *testing.T) { }, } - resolved, err := Resolve( + resolved, _, err := Resolve( context.Background(), getter, nil, @@ -563,7 +563,7 @@ func TestResolve_DeploymentVariableValueUsedWhenNoResourceVar(t *testing.T) { resourceVars: map[string][]oapi.ResourceVariable{}, } - resolved, err := Resolve( + resolved, _, err := Resolve( context.Background(), getter, nil, @@ -603,7 +603,7 @@ func TestResolve_DefaultValueFallback(t *testing.T) { resourceVars: map[string][]oapi.ResourceVariable{}, } - resolved, err := Resolve( + resolved, _, err := Resolve( context.Background(), getter, nil, @@ -637,7 +637,7 @@ func TestResolve_NoMatchNoDefault_KeyAbsent(t *testing.T) { resourceVars: map[string][]oapi.ResourceVariable{}, } - resolved, err := Resolve( + resolved, _, err := Resolve( context.Background(), getter, nil, @@ -688,7 +688,7 @@ func TestResolve_HighestPriorityValueWins(t *testing.T) { resourceVars: map[string][]oapi.ResourceVariable{}, } - resolved, err := Resolve( + resolved, _, err := Resolve( context.Background(), getter, nil, @@ -890,7 +890,7 @@ func TestResolve_DeploymentVarValue_DefaultAndSelectorGatedOverride(t *testing.T candidates: candidates, } - resolved, err := Resolve( + resolved, _, err := Resolve( context.Background(), getter, nil, @@ -923,7 +923,7 @@ func TestResolve_MultipleVariables(t *testing.T) { resourceVars: map[string][]oapi.ResourceVariable{}, } - resolved, err := Resolve( + resolved, _, err := Resolve( context.Background(), getter, nil, @@ -951,7 +951,7 @@ func TestResolve_MultipleVariables(t *testing.T) { func TestResolve_NoDeploymentVars_EmptyMap(t *testing.T) { scope := newScope() - resolved, err := Resolve( + resolved, _, err := Resolve( context.Background(), emptyGetter, nil, @@ -1033,7 +1033,7 @@ func TestResolve_ResourceVar_WithReference(t *testing.T) { }, } - resolved, err := Resolve( + resolved, _, err := Resolve( context.Background(), getter, nil, @@ -1115,7 +1115,7 @@ func TestResolve_DeploymentVarValue_WithReference(t *testing.T) { }, } - resolved, err := Resolve( + resolved, _, err := Resolve( context.Background(), getter, nil, @@ -1213,7 +1213,7 @@ func TestResolve_MixedLiteralAndReference(t *testing.T) { }, } - resolved, err := Resolve( + resolved, _, err := Resolve( context.Background(), getter, nil, @@ -1263,7 +1263,7 @@ func TestResolve_ResourceVarRefFails_FallsToDeploymentValue(t *testing.T) { rules: []eval.Rule{}, } - resolved, err := Resolve( + resolved, _, err := Resolve( context.Background(), getter, nil, @@ -1298,7 +1298,7 @@ func TestResolveValue_Reference_ResourceConfig(t *testing.T) { } val := referenceValue("self", "config", "networking", "vpc_id") - lv, err := ResolveValue( + lv, _, err := ResolveValue( context.Background(), resolver, nil, @@ -1323,7 +1323,7 @@ func TestResolveValue_Sensitive_ReturnsError(t *testing.T) { _ = v.FromSensitiveValue(oapi.SensitiveValue{ValueHash: "abc123"}) entity := makeResourceEntity(scope.Resource) - _, err := ResolveValue( + _, _, err := ResolveValue( context.Background(), emptyResolver, nil, @@ -1394,7 +1394,7 @@ func TestResolve_VariableSet_SimpleInjection(t *testing.T) { }, } - resolved, err := Resolve( + resolved, _, err := Resolve( context.Background(), getter, nil, @@ -1445,7 +1445,7 @@ func TestResolve_VariableSet_DoesNotOverwriteResourceVar(t *testing.T) { }, } - resolved, err := Resolve( + resolved, _, err := Resolve( context.Background(), getter, nil, @@ -1496,7 +1496,7 @@ func TestResolve_VariableSet_DoesNotOverwriteDeploymentVarValue(t *testing.T) { }, } - resolved, err := Resolve( + resolved, _, err := Resolve( context.Background(), getter, nil, @@ -1557,7 +1557,7 @@ func TestResolve_VariableSet_HighestPriorityWins(t *testing.T) { }, } - resolved, err := Resolve( + resolved, _, err := Resolve( context.Background(), getter, nil, @@ -1602,7 +1602,7 @@ func TestResolve_VariableSet_UnrelatedDoNotMatch(t *testing.T) { }, } - resolved, err := Resolve( + resolved, _, err := Resolve( context.Background(), getter, nil, diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/value.go b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/value.go index 6ff2803fd..5b5c8d5dc 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/value.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/value.go @@ -30,6 +30,10 @@ type SecretResolver interface { // path on the matched entity. Secret references are fetched through the // SecretResolver and returned as string literals. Sensitive values without a // concrete provider reference remain unresolvable. +// +// The boolean return is true when the resolved value originated from a +// secret_ref. Callers use it to populate release.EncryptedVariables so that +// downstream consumers can mark the value as sensitive in logs and UI. func ResolveValue( ctx context.Context, resolver RelatedEntityResolver, @@ -38,26 +42,31 @@ func ResolveValue( resourceID string, entity *oapi.RelatableEntity, value *oapi.Value, -) (*oapi.LiteralValue, error) { +) (*oapi.LiteralValue, bool, error) { ctx, span := tracer.Start(ctx, "variableresolver.ResolveValue") defer span.End() valueType, err := value.GetType() if err != nil { - return nil, fmt.Errorf("determine value type: %w", err) + return nil, false, fmt.Errorf("determine value type: %w", err) } switch valueType { case "literal": - return resolveLiteral(value) + lv, err := resolveLiteral(value) + return lv, false, err case "reference": - return resolveReference(ctx, resolver, value, entity) + lv, err := resolveReference(ctx, resolver, value, entity) + return lv, false, err case "secret_ref": - return resolveSecretReference(ctx, secretResolver, workspaceID, value) + lv, err := resolveSecretReference(ctx, secretResolver, workspaceID, value) + return lv, err == nil, err case "sensitive": - return nil, fmt.Errorf("sensitive values are not resolved by the variable resolver") + return nil, false, fmt.Errorf( + "sensitive values are not resolved by the variable resolver", + ) default: - return nil, fmt.Errorf("unsupported value type: %s", valueType) + return nil, false, fmt.Errorf("unsupported value type: %s", valueType) } } From 817e39a75908a23b6da7f26c9cd0bb755f9cda33 Mon Sep 17 00:00:00 2001 From: Michael Leone Date: Mon, 11 May 2026 14:56:42 -0400 Subject: [PATCH 07/13] test(secrets): add end-to-end coverage for secret_ref resolution Test infrastructure: - harness.FakeSecretResolver and harness.FailingSecretResolver satisfy variableresolver.SecretResolver. The fake stores canned (provider, path, key) -> value entries and records every Resolve call; the failing variant returns the injected error for every call. - WithSecretResolver(...) PipelineOption threads the resolver into desiredrelease.NewController through ScenarioState. - SecretRefValue(provider, key, path...) builds an oapi.Value carrying a SecretReferenceValue so scenarios can declare secret_ref entries with the same ergonomics as DefaultValue / WithVariableValue. - ProcessDesiredReleasesErr() returns the reconcile error instead of failing the test, so negative-path tests can assert on it. - AssertReleaseEncryptedVariables asserts the set of keys recorded on release.EncryptedVariables, order-independent. Test cases (test/controllers/secret_ref_test.go): - Happy path: resolved plaintext lands on release.Variables and the key is appended to release.EncryptedVariables. Resolver receives the expected provider / joined path / key. - Mixed literal + secret_ref: EncryptedVariables contains only the secret_ref-origin keys. - Empty SecretPath: the SecretReference.Path passed to the resolver is empty, matching providers like env. - Resolver outage: the reconciler propagates the upstream error and AssertNoRelease holds. Mirrors the Phase-5 design point that re-resolve-each-dispatch means an outage blocks the release rather than silently shipping a release without the variable. - No resolver configured: a clear "no SecretResolver configured" error surfaces; no release is produced. Production fixes uncovered while writing the tests: - variableresolver: priority-cascade helpers used to swallow ResolveValue errors. Fine for literal/reference (try the next candidate) but wrong for secret_ref - a Doppler / AWS outage was silently producing a release with the secret variable missing instead of blocking dispatch. Added an ErrSecretResolution sentinel that every secret_ref error path wraps, and the helpers now propagate any error matching it via a new third return value. Non-secret errors keep the previous fallthrough behavior. variableresolver.Resolve wraps the propagated error with the affected variable key. - desiredrelease.Controller / reconciler now hold variableresolver.SecretResolver (interface) instead of *secrets.Resolver (concrete). *secrets.Resolver still satisfies the interface so main.go is unchanged, but the harness can inject fakes without pulling pkg/db or pkg/crypto into the test binary. --- .../controllers/desiredrelease/controller.go | 9 +- .../controllers/desiredrelease/reconcile.go | 5 +- .../variableresolver/resolve.go | 65 +++++--- .../desiredrelease/variableresolver/value.go | 17 +- .../test/controllers/harness/assertions.go | 10 ++ .../test/controllers/harness/pipeline.go | 15 +- .../test/controllers/harness/secrets.go | 102 ++++++++++++ .../test/controllers/secret_ref_test.go | 149 ++++++++++++++++++ 8 files changed, 344 insertions(+), 28 deletions(-) create mode 100644 apps/workspace-engine/test/controllers/harness/secrets.go create mode 100644 apps/workspace-engine/test/controllers/secret_ref_test.go diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/controller.go b/apps/workspace-engine/svc/controllers/desiredrelease/controller.go index 8b03d4e3e..6ac1f687c 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/controller.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/controller.go @@ -20,6 +20,7 @@ import ( "workspace-engine/pkg/store/policies" "workspace-engine/pkg/store/releasetargets" "workspace-engine/svc" + "workspace-engine/svc/controllers/desiredrelease/variableresolver" ) var tracer = otel.Tracer("workspace-engine/svc/controllers/desiredrelease") @@ -29,7 +30,7 @@ type Controller struct { getter Getter queries *db.Queries setter Setter - secretResolver *secrets.Resolver + secretResolver variableresolver.SecretResolver } // Process implements [reconcile.Processor]. @@ -98,7 +99,11 @@ func (c *Controller) Process(ctx context.Context, item reconcile.Item) (reconcil // NewController creates a Controller with the given dependencies. // Use this constructor in tests to inject mock implementations. -func NewController(getter Getter, setter Setter, secretResolver *secrets.Resolver) *Controller { +func NewController( + getter Getter, + setter Setter, + secretResolver variableresolver.SecretResolver, +) *Controller { return &Controller{getter: getter, setter: setter, secretResolver: secretResolver} } diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go b/apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go index 0fcad4e52..bd4a60943 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go @@ -10,7 +10,6 @@ import ( "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" "workspace-engine/pkg/oapi" - "workspace-engine/pkg/secrets" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector" "workspace-engine/svc/controllers/desiredrelease/policyeval" @@ -29,7 +28,7 @@ type reconciler struct { getter Getter setter Setter - secretResolver *secrets.Resolver + secretResolver variableresolver.SecretResolver rt *ReleaseTarget scope *evaluator.EvaluatorScope @@ -117,7 +116,7 @@ func Reconcile( workspaceID string, getter Getter, setter Setter, - secretResolver *secrets.Resolver, + secretResolver variableresolver.SecretResolver, rt *ReleaseTarget, ) (*ReconcileResult, error) { ctx, span := tracer.Start(ctx, "desiredrelease.Reconcile") diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go index f58eb8415..f9c14818b 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go @@ -2,6 +2,7 @@ package variableresolver import ( "context" + "errors" "fmt" "sort" @@ -113,7 +114,7 @@ func Resolve( for _, dv := range deploymentVars { key := dv.Variable.Key - if lv, sensitive := resolveFromResource( + lv, sensitive, err := resolveFromResource( ctx, resolver, secretResolver, @@ -123,7 +124,13 @@ func Resolve( resourceVars, scope.Resource, entity, - ); lv != nil { + ) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, "resolve variable from resource failed") + return nil, nil, fmt.Errorf("resolve variable %q: %w", key, err) + } + if lv != nil { resolved[key] = *lv if sensitive { sensitiveKeys = append(sensitiveKeys, key) @@ -132,7 +139,7 @@ func Resolve( continue } - if lv, sensitive := resolveFromValues( + lv, sensitive, err = resolveFromValues( ctx, resolver, secretResolver, @@ -141,7 +148,13 @@ func Resolve( dv.Values, scope.Resource, entity, - ); lv != nil { + ) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, "resolve variable from values failed") + return nil, nil, fmt.Errorf("resolve variable %q: %w", key, err) + } + if lv != nil { resolved[key] = *lv if sensitive { sensitiveKeys = append(sensitiveKeys, key) @@ -150,7 +163,7 @@ func Resolve( continue } - if lv, sensitive := resolveFromVariableSets( + lv, sensitive, err = resolveFromVariableSets( ctx, key, filteredVariableSets, @@ -159,7 +172,13 @@ func Resolve( wsID, resourceID, entity, - ); lv != nil { + ) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, "resolve variable from variable sets failed") + return nil, nil, fmt.Errorf("resolve variable %q: %w", key, err) + } + if lv != nil { resolved[key] = *lv if sensitive { sensitiveKeys = append(sensitiveKeys, key) @@ -167,7 +186,6 @@ func Resolve( fromVariableSet++ continue } - } span.SetAttributes( @@ -275,10 +293,10 @@ func resolveFromResource( resourceVars map[string][]oapi.ResourceVariable, resource *oapi.Resource, entity *oapi.RelatableEntity, -) (*oapi.LiteralValue, bool) { +) (*oapi.LiteralValue, bool, error) { candidates, ok := resourceVars[key] if !ok || len(candidates) == 0 { - return nil, false + return nil, false, nil } matched := make([]oapi.ResourceVariable, 0, len(candidates)) @@ -292,7 +310,7 @@ func resolveFromResource( } } if len(matched) == 0 { - return nil, false + return nil, false, nil } sort.Slice(matched, func(i, j int) bool { @@ -309,11 +327,14 @@ func resolveFromResource( entity, &rv.Value, ) + if errors.Is(err, ErrSecretResolution) { + return nil, false, err + } if err == nil && lv != nil { - return lv, sensitive + return lv, sensitive, nil } } - return nil, false + return nil, false, nil } // resolveFromValues finds the highest-priority deployment variable value @@ -327,7 +348,7 @@ func resolveFromValues( values []oapi.DeploymentVariableValue, resource *oapi.Resource, entity *oapi.RelatableEntity, -) (*oapi.LiteralValue, bool) { +) (*oapi.LiteralValue, bool, error) { matched := make([]oapi.DeploymentVariableValue, 0, len(values)) for _, v := range values { if v.ResourceSelector == nil { @@ -340,7 +361,7 @@ func resolveFromValues( } } if len(matched) == 0 { - return nil, false + return nil, false, nil } sort.Slice(matched, func(i, j int) bool { @@ -357,11 +378,14 @@ func resolveFromValues( entity, &v.Value, ) + if errors.Is(err, ErrSecretResolution) { + return nil, false, err + } if err == nil && lv != nil { - return lv, sensitive + return lv, sensitive, nil } } - return nil, false + return nil, false, nil } func resolveFromVariableSets( @@ -373,7 +397,7 @@ func resolveFromVariableSets( workspaceID uuid.UUID, resourceID string, entity *oapi.RelatableEntity, -) (*oapi.LiteralValue, bool) { +) (*oapi.LiteralValue, bool, error) { for _, vs := range variableSets { for _, v := range vs.Variables { if v.Key == key { @@ -386,13 +410,16 @@ func resolveFromVariableSets( entity, &v.Value, ) + if errors.Is(err, ErrSecretResolution) { + return nil, false, err + } if err == nil && lv != nil { - return lv, sensitive + return lv, sensitive, nil } } } } - return nil, false + return nil, false, nil } func filterVariableSets( diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/value.go b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/value.go index 5b5c8d5dc..804d68202 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/value.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/value.go @@ -2,6 +2,7 @@ package variableresolver import ( "context" + "errors" "fmt" "github.com/google/uuid" @@ -10,6 +11,12 @@ import ( "workspace-engine/pkg/workspace/relationships" ) +// ErrSecretResolution is wrapped by every error originating from secret_ref +// resolution so the priority-cascade helpers can distinguish a transient +// candidate-skip (literal/reference) from a fatal upstream failure that must +// block the release. +var ErrSecretResolution = errors.New("secret resolution failed") + // RelatedEntityResolver resolves a reference name to the matched related // entities for a resource. Implementations may evaluate relationship rules // in realtime or return pre-computed/mocked results. @@ -77,11 +84,14 @@ func resolveSecretReference( value *oapi.Value, ) (*oapi.LiteralValue, error) { if secretResolver == nil { - return nil, fmt.Errorf("secret_ref encountered but no SecretResolver configured") + return nil, fmt.Errorf( + "%w: no SecretResolver configured (VARIABLES_AES_256_KEY unset?)", + ErrSecretResolution, + ) } srv, err := value.AsSecretReferenceValue() if err != nil { - return nil, fmt.Errorf("extract secret reference value: %w", err) + return nil, fmt.Errorf("%w: extract secret reference value: %w", ErrSecretResolution, err) } ref := secrets.SecretReference{ Provider: srv.SecretProvider, @@ -99,7 +109,8 @@ func resolveSecretReference( plaintext, err := secretResolver.Resolve(ctx, workspaceID, ref) if err != nil { return nil, fmt.Errorf( - "resolve secret %s/%s/%s: %w", + "%w: %s/%s/%s: %w", + ErrSecretResolution, ref.Provider, ref.Path, ref.Key, diff --git a/apps/workspace-engine/test/controllers/harness/assertions.go b/apps/workspace-engine/test/controllers/harness/assertions.go index 24e9ab125..b61b77ec2 100644 --- a/apps/workspace-engine/test/controllers/harness/assertions.go +++ b/apps/workspace-engine/test/controllers/harness/assertions.go @@ -93,6 +93,16 @@ func (p *TestPipeline) ReleaseVariables(t *testing.T, idx int) map[string]oapi.L return p.ReleaseSetter.Releases[idx].Variables } +// AssertReleaseEncryptedVariables asserts the set of variable keys marked as +// originating from a secret_ref on the release at the given index. Order is +// not significant. +func (p *TestPipeline) AssertReleaseEncryptedVariables(t *testing.T, idx int, keys ...string) { + t.Helper() + require.Greater(t, len(p.ReleaseSetter.Releases), idx, + "release index %d out of range (have %d)", idx, len(p.ReleaseSetter.Releases)) + assert.ElementsMatch(t, keys, p.ReleaseSetter.Releases[idx].EncryptedVariables) +} + // --------------------------------------------------------------------------- // Job assertions // --------------------------------------------------------------------------- diff --git a/apps/workspace-engine/test/controllers/harness/pipeline.go b/apps/workspace-engine/test/controllers/harness/pipeline.go index 861325c13..4d723066d 100644 --- a/apps/workspace-engine/test/controllers/harness/pipeline.go +++ b/apps/workspace-engine/test/controllers/harness/pipeline.go @@ -10,6 +10,7 @@ import ( "workspace-engine/pkg/workspace/relationships/eval" selectoreval "workspace-engine/svc/controllers/deploymentresourceselectoreval" "workspace-engine/svc/controllers/desiredrelease" + "workspace-engine/svc/controllers/desiredrelease/variableresolver" "workspace-engine/svc/controllers/jobdispatch" ) @@ -79,6 +80,8 @@ type ScenarioState struct { ResourceVars map[string][]oapi.ResourceVariable RelationshipRules []eval.Rule Candidates map[string][]eval.EntityData + + SecretResolver variableresolver.SecretResolver } type ResourceDef struct { @@ -150,7 +153,7 @@ func NewTestPipeline(t *testing.T, opts ...PipelineOption) *TestPipeline { releaseSetter.Agents = sc.JobAgents selectorCtrl := selectoreval.NewController(selectorGetter, selectorSetter, qs.shared) - releaseCtrl := desiredrelease.NewController(releaseGetter, releaseSetter, nil) + releaseCtrl := desiredrelease.NewController(releaseGetter, releaseSetter, sc.SecretResolver) sel := "true" jobDispatchGetter := &JobDispatchGetter{ @@ -228,6 +231,16 @@ func (p *TestPipeline) ProcessDesiredReleases() int { return res.Processed } +// ProcessDesiredReleasesErr claims and processes all pending desired-release +// items and returns the processor error without failing the test. Use this +// in tests that assert a controller-level failure (e.g. secret resolution +// outage blocks the release). +func (p *TestPipeline) ProcessDesiredReleasesErr() error { + p.t.Helper() + _, err := DrainQueue(context.Background(), p.releaseQueue, p.releaseCtrl) + return err +} + // ProcessJobDispatches claims and processes all pending job-dispatch items. // Returns the count processed. func (p *TestPipeline) ProcessJobDispatches() int { diff --git a/apps/workspace-engine/test/controllers/harness/secrets.go b/apps/workspace-engine/test/controllers/harness/secrets.go new file mode 100644 index 000000000..828771628 --- /dev/null +++ b/apps/workspace-engine/test/controllers/harness/secrets.go @@ -0,0 +1,102 @@ +package harness + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "workspace-engine/pkg/oapi" + "workspace-engine/pkg/secrets" + "workspace-engine/svc/controllers/desiredrelease/variableresolver" +) + +// FakeSecretResolver satisfies variableresolver.SecretResolver with a +// canned in-memory map keyed by (provider, path, key). Use +// FailingSecretResolver to assert error propagation. +type FakeSecretResolver struct { + Entries map[string]string + Calls []secrets.SecretReference + WorkspaceIDs []uuid.UUID +} + +func fakeRefKey(provider, path, key string) string { + return provider + "|" + path + "|" + key +} + +// NewFakeSecretResolver constructs a resolver with no canned entries. Use +// Set to populate. +func NewFakeSecretResolver() *FakeSecretResolver { + return &FakeSecretResolver{Entries: make(map[string]string)} +} + +// Set seeds the resolver with a canned value for the given ref. +func (f *FakeSecretResolver) Set(provider, path, key, value string) { + f.Entries[fakeRefKey(provider, path, key)] = value +} + +// Resolve implements variableresolver.SecretResolver. +func (f *FakeSecretResolver) Resolve( + _ context.Context, + workspaceID uuid.UUID, + ref secrets.SecretReference, +) (string, error) { + f.Calls = append(f.Calls, ref) + f.WorkspaceIDs = append(f.WorkspaceIDs, workspaceID) + v, ok := f.Entries[fakeRefKey(ref.Provider, ref.Path, ref.Key)] + if !ok { + return "", fmt.Errorf( + "fake secret resolver: no entry for %s/%s/%s", + ref.Provider, + ref.Path, + ref.Key, + ) + } + return v, nil +} + +var _ variableresolver.SecretResolver = (*FakeSecretResolver)(nil) + +// FailingSecretResolver returns the same error for every Resolve call. Use it +// in tests asserting how the reconciler handles secret resolution failures. +type FailingSecretResolver struct { + Err error + Calls []secrets.SecretReference +} + +// Resolve implements variableresolver.SecretResolver. +func (f *FailingSecretResolver) Resolve( + _ context.Context, + _ uuid.UUID, + ref secrets.SecretReference, +) (string, error) { + f.Calls = append(f.Calls, ref) + return "", f.Err +} + +var _ variableresolver.SecretResolver = (*FailingSecretResolver)(nil) + +// WithSecretResolver injects a SecretResolver into the pipeline so the +// desired-release controller can resolve variable_value rows of kind +// secret_ref. The resolver is consumed by ResolveValue during +// variableresolver.Resolve. +func WithSecretResolver(r variableresolver.SecretResolver) PipelineOption { + return func(sc *ScenarioState) { + sc.SecretResolver = r + } +} + +// SecretRefValue builds an oapi.Value carrying a SecretReferenceValue. Path +// is optional; pass zero or more components. +func SecretRefValue(provider, key string, path ...string) oapi.Value { + srv := oapi.SecretReferenceValue{ + SecretProvider: provider, + SecretKey: key, + } + if len(path) > 0 { + p := append([]string(nil), path...) + srv.SecretPath = &p + } + v := oapi.Value{} + _ = v.FromSecretReferenceValue(srv) + return v +} diff --git a/apps/workspace-engine/test/controllers/secret_ref_test.go b/apps/workspace-engine/test/controllers/secret_ref_test.go new file mode 100644 index 000000000..034e50132 --- /dev/null +++ b/apps/workspace-engine/test/controllers/secret_ref_test.go @@ -0,0 +1,149 @@ +package controllers_test + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + . "workspace-engine/test/controllers/harness" +) + +// TestSecretRef_Resolved_FlowsThroughRelease covers the happy path: a +// variable_value of kind secret_ref → variableresolver.Resolve calls the +// injected SecretResolver → resolved plaintext lands on release.Variables +// and the variable key is appended to release.EncryptedVariables. +func TestSecretRef_Resolved_FlowsThroughRelease(t *testing.T) { + fake := NewFakeSecretResolver() + fake.Set("doppler-platform", "backend/production", "ARGOCD_TOKEN", "resolved-token-value") + + p := NewTestPipeline(t, + WithDeployment(DeploymentSelector("true")), + WithEnvironment(EnvironmentName("production")), + WithResource(ResourceName("srv"), ResourceKind("Server")), + WithVersion(VersionTag("v1.0.0")), + WithDeploymentVariable("argocd_token", + WithVariableValue(SecretRefValue( + "doppler-platform", + "ARGOCD_TOKEN", + "backend", "production", + )), + ), + WithSecretResolver(fake), + ) + p.Run() + + p.AssertReleaseCreated(t) + p.AssertReleaseVariableCount(t, 0, 1) + p.AssertReleaseVariableEquals(t, 0, "argocd_token", "resolved-token-value") + p.AssertReleaseEncryptedVariables(t, 0, "argocd_token") + + require.Len(t, fake.Calls, 1, "expected secret resolver to be called once") + assert.Equal(t, "doppler-platform", fake.Calls[0].Provider) + assert.Equal(t, "backend/production", fake.Calls[0].Path) + assert.Equal(t, "ARGOCD_TOKEN", fake.Calls[0].Key) +} + +// TestSecretRef_MixedWithLiteral verifies that EncryptedVariables only +// contains the secret_ref-originated keys, not literals. +func TestSecretRef_MixedWithLiteral(t *testing.T) { + fake := NewFakeSecretResolver() + fake.Set("aws-prod", "prod/db", "password", "hunter2") + + p := NewTestPipeline(t, + WithDeployment(DeploymentSelector("true")), + WithEnvironment(EnvironmentName("production")), + WithResource(ResourceName("srv"), ResourceKind("Server")), + WithVersion(VersionTag("v1.0.0")), + WithDeploymentVariable("image", DefaultValue("nginx:latest")), + WithDeploymentVariable("db_password", + WithVariableValue(SecretRefValue("aws-prod", "password", "prod/db")), + ), + WithSecretResolver(fake), + ) + p.Run() + + p.AssertReleaseCreated(t) + p.AssertReleaseVariableCount(t, 0, 2) + p.AssertReleaseVariableEquals(t, 0, "image", "nginx:latest") + p.AssertReleaseVariableEquals(t, 0, "db_password", "hunter2") + p.AssertReleaseEncryptedVariables(t, 0, "db_password") +} + +// TestSecretRef_NoPath covers providers whose reference does not carry a +// path component (e.g. env). The SecretReference passed to the resolver has +// an empty Path. +func TestSecretRef_NoPath(t *testing.T) { + fake := NewFakeSecretResolver() + fake.Set("env-defaults", "", "LICENSE_KEY", "abc-123") + + p := NewTestPipeline(t, + WithDeployment(DeploymentSelector("true")), + WithEnvironment(EnvironmentName("production")), + WithResource(ResourceName("srv"), ResourceKind("Server")), + WithVersion(VersionTag("v1.0.0")), + WithDeploymentVariable("license_key", + WithVariableValue(SecretRefValue("env-defaults", "LICENSE_KEY")), + ), + WithSecretResolver(fake), + ) + p.Run() + + p.AssertReleaseCreated(t) + p.AssertReleaseVariableEquals(t, 0, "license_key", "abc-123") + p.AssertReleaseEncryptedVariables(t, 0, "license_key") + + require.Len(t, fake.Calls, 1) + assert.Empty(t, fake.Calls[0].Path) +} + +// TestSecretRef_ResolverError_NoRelease covers the failure path: a +// provider outage (or any resolver error) propagates up and blocks the +// release. desiredrelease.Reconcile must not persist a release in that +// case — Phase 5 was explicit that re-resolve-each-dispatch means an +// outage is observable as a stuck reconcile, not a silent literal. +func TestSecretRef_ResolverError_NoRelease(t *testing.T) { + failing := &FailingSecretResolver{Err: errors.New("upstream 503")} + + p := NewTestPipeline(t, + WithDeployment(DeploymentSelector("true")), + WithEnvironment(EnvironmentName("production")), + WithResource(ResourceName("srv"), ResourceKind("Server")), + WithVersion(VersionTag("v1.0.0")), + WithDeploymentVariable("db_password", + WithVariableValue(SecretRefValue("doppler-prod", "DB_PASSWORD", "backend/prod")), + ), + WithSecretResolver(failing), + ) + p.EnqueueSelectorEval() + p.ProcessSelectorEvals() + err := p.ProcessDesiredReleasesErr() + + require.Error(t, err, "expected reconcile to propagate the upstream failure") + assert.Contains(t, err.Error(), "upstream 503") + p.AssertNoRelease(t) + require.Len(t, failing.Calls, 1, + "secret resolver must be called once before the failure surfaces") +} + +// TestSecretRef_NoResolverConfigured covers the case where a secret_ref is +// encountered but no resolver was wired (e.g. VARIABLES_AES_256_KEY unset +// on the workspace-engine). The release is blocked with a clear error. +func TestSecretRef_NoResolverConfigured(t *testing.T) { + p := NewTestPipeline(t, + WithDeployment(DeploymentSelector("true")), + WithEnvironment(EnvironmentName("production")), + WithResource(ResourceName("srv"), ResourceKind("Server")), + WithVersion(VersionTag("v1.0.0")), + WithDeploymentVariable("db_password", + WithVariableValue(SecretRefValue("doppler-prod", "DB_PASSWORD", "backend/prod")), + ), + ) + p.EnqueueSelectorEval() + p.ProcessSelectorEvals() + err := p.ProcessDesiredReleasesErr() + + require.Error(t, err, "expected reconcile to fail with no SecretResolver wired") + assert.Contains(t, err.Error(), "no SecretResolver configured") + p.AssertNoRelease(t) +} From c6f12ef649808df5cde7fd6c2c49e30aaaa1fdf0 Mon Sep 17 00:00:00 2001 From: Michael Leone Date: Mon, 11 May 2026 15:03:57 -0400 Subject: [PATCH 08/13] feat(secrets): add caching layer * adds caching layer by provider/workspace on both value and provider --- apps/workspace-engine/main.go | 1 + apps/workspace-engine/pkg/config/env.go | 6 + .../pkg/secrets/provider_cache.go | 97 +++++++++++++ .../pkg/secrets/provider_cache_test.go | 103 ++++++++++++++ apps/workspace-engine/pkg/secrets/resolver.go | 92 ++++++++++--- .../pkg/secrets/resolver_test.go | 128 +++++++++++++++++- 6 files changed, 404 insertions(+), 23 deletions(-) create mode 100644 apps/workspace-engine/pkg/secrets/provider_cache.go create mode 100644 apps/workspace-engine/pkg/secrets/provider_cache_test.go diff --git a/apps/workspace-engine/main.go b/apps/workspace-engine/main.go index 5fac280b0..3f228528f 100644 --- a/apps/workspace-engine/main.go +++ b/apps/workspace-engine/main.go @@ -120,5 +120,6 @@ func newSecretResolver(ctx context.Context) *secrets.Resolver { store, providers.NewDefaultRegistry(), secrets.NewCache(config.Global.SecretsCacheTTL), + secrets.NewProviderCache(config.Global.SecretsProviderCacheTTL), ) } diff --git a/apps/workspace-engine/pkg/config/env.go b/apps/workspace-engine/pkg/config/env.go index 3a4485a08..f0ddbe1dd 100644 --- a/apps/workspace-engine/pkg/config/env.go +++ b/apps/workspace-engine/pkg/config/env.go @@ -61,6 +61,12 @@ type Config struct { // TTL for the secrets resolver value cache. SecretsCacheTTL time.Duration `default:"5m" envconfig:"SECRETS_CACHE_TTL"` + + // TTL for the secrets resolver provider-instance cache (constructed + // Provider objects, e.g. AWS SDK clients). A longer TTL is appropriate + // here than the value cache because provider configs change rarely + // while individual secret values may be rotated more often. + SecretsProviderCacheTTL time.Duration `default:"30m" envconfig:"SECRETS_PROVIDER_CACHE_TTL"` } // GetMaxConcurrency returns the max concurrency for a given service kind. diff --git a/apps/workspace-engine/pkg/secrets/provider_cache.go b/apps/workspace-engine/pkg/secrets/provider_cache.go new file mode 100644 index 000000000..dac79a9ba --- /dev/null +++ b/apps/workspace-engine/pkg/secrets/provider_cache.go @@ -0,0 +1,97 @@ +package secrets + +import ( + "sync" + "time" + + "github.com/google/uuid" +) + +// providerCacheKey identifies a constructed Provider instance scoped to a +// workspace + secret_provider row name. +type providerCacheKey struct { + WorkspaceID uuid.UUID + Name string +} + +type providerCacheEntry struct { + provider Provider + expiresAt time.Time +} + +// ProviderCache memoizes constructed Provider instances per +// (workspaceID, providerName) so that hot release fan-outs do not pay for +// repeated config decryption + factory construction (AWS LoadDefaultConfig, +// HTTP client rebuilds, etc.). +type ProviderCache struct { + ttl time.Duration + now func() time.Time + mu sync.RWMutex + entries map[providerCacheKey]providerCacheEntry +} + +// NewProviderCache constructs a cache. A non-positive TTL disables caching. +func NewProviderCache(ttl time.Duration) *ProviderCache { + return &ProviderCache{ + ttl: ttl, + now: time.Now, + entries: make(map[providerCacheKey]providerCacheEntry), + } +} + +func providerKeyFor(workspaceID uuid.UUID, providerName string) providerCacheKey { + return providerCacheKey{WorkspaceID: workspaceID, Name: providerName} +} + +// Get returns the cached Provider if present and unexpired. +func (c *ProviderCache) Get(workspaceID uuid.UUID, providerName string) (Provider, bool) { + if c.ttl <= 0 { + return nil, false + } + c.mu.RLock() + entry, ok := c.entries[providerKeyFor(workspaceID, providerName)] + c.mu.RUnlock() + if !ok { + return nil, false + } + if c.now().After(entry.expiresAt) { + return nil, false + } + return entry.provider, true +} + +// Set stores the constructed Provider with the cache TTL applied. +func (c *ProviderCache) Set(workspaceID uuid.UUID, providerName string, p Provider) { + if c.ttl <= 0 { + return + } + c.mu.Lock() + c.entries[providerKeyFor(workspaceID, providerName)] = providerCacheEntry{ + provider: p, + expiresAt: c.now().Add(c.ttl), + } + c.mu.Unlock() +} + +// Invalidate drops the Provider for the named provider in the given +// workspace. Wired into LISTEN/NOTIFY so an upstream config change forces +// reconstruction on the next resolve. +func (c *ProviderCache) Invalidate(workspaceID uuid.UUID, providerName string) { + c.mu.Lock() + delete(c.entries, providerKeyFor(workspaceID, providerName)) + c.mu.Unlock() +} + +// InvalidateAll drops every cached Provider. +func (c *ProviderCache) InvalidateAll() { + c.mu.Lock() + c.entries = make(map[providerCacheKey]providerCacheEntry) + c.mu.Unlock() +} + +// Size returns the number of entries currently cached. +func (c *ProviderCache) Size() int { + c.mu.RLock() + defer c.mu.RUnlock() + return len(c.entries) +} diff --git a/apps/workspace-engine/pkg/secrets/provider_cache_test.go b/apps/workspace-engine/pkg/secrets/provider_cache_test.go new file mode 100644 index 000000000..d0bc0af1f --- /dev/null +++ b/apps/workspace-engine/pkg/secrets/provider_cache_test.go @@ -0,0 +1,103 @@ +package secrets + +import ( + "context" + "testing" + "time" + + "github.com/google/uuid" +) + +type stubProviderInst struct { + typ string +} + +func (s *stubProviderInst) Type() string { return s.typ } +func (s *stubProviderInst) Resolve(_ context.Context, _ SecretReference) (string, error) { + return "", nil +} + +func TestProviderCacheHitMiss(t *testing.T) { + c := NewProviderCache(time.Minute) + ws := uuid.New() + + if _, ok := c.Get(ws, "doppler-prod"); ok { + t.Fatal("expected miss on empty cache") + } + + want := &stubProviderInst{typ: "doppler"} + c.Set(ws, "doppler-prod", want) + + got, ok := c.Get(ws, "doppler-prod") + if !ok { + t.Fatal("expected hit after Set") + } + if got != want { + t.Fatalf("got %p want %p", got, want) + } +} + +func TestProviderCacheExpiry(t *testing.T) { + c := NewProviderCache(time.Minute) + now := time.Unix(1_700_000_000, 0) + c.now = func() time.Time { return now } + ws := uuid.New() + + c.Set(ws, "doppler-prod", &stubProviderInst{typ: "doppler"}) + if _, ok := c.Get(ws, "doppler-prod"); !ok { + t.Fatal("expected hit immediately after Set") + } + + now = now.Add(time.Minute + time.Second) + if _, ok := c.Get(ws, "doppler-prod"); ok { + t.Fatal("expected miss after TTL expiry") + } +} + +func TestProviderCacheDisabledWhenTTLZero(t *testing.T) { + c := NewProviderCache(0) + ws := uuid.New() + + c.Set(ws, "doppler-prod", &stubProviderInst{typ: "doppler"}) + if _, ok := c.Get(ws, "doppler-prod"); ok { + t.Fatal("zero TTL must disable caching") + } + if c.Size() != 0 { + t.Fatalf("zero TTL must not store entries, got %d", c.Size()) + } +} + +func TestProviderCacheInvalidate(t *testing.T) { + c := NewProviderCache(time.Minute) + wsA := uuid.New() + wsB := uuid.New() + + c.Set(wsA, "doppler-prod", &stubProviderInst{typ: "doppler"}) + c.Set(wsA, "aws-prod", &stubProviderInst{typ: "aws_secrets_manager"}) + c.Set(wsB, "doppler-prod", &stubProviderInst{typ: "doppler"}) + + c.Invalidate(wsA, "doppler-prod") + + if _, ok := c.Get(wsA, "doppler-prod"); ok { + t.Fatal("expected wsA/doppler-prod evicted") + } + if _, ok := c.Get(wsA, "aws-prod"); !ok { + t.Fatal("expected wsA/aws-prod retained") + } + if _, ok := c.Get(wsB, "doppler-prod"); !ok { + t.Fatal("expected wsB/doppler-prod retained (different workspace)") + } +} + +func TestProviderCacheInvalidateAll(t *testing.T) { + c := NewProviderCache(time.Minute) + ws := uuid.New() + c.Set(ws, "doppler-prod", &stubProviderInst{typ: "doppler"}) + c.Set(ws, "aws-prod", &stubProviderInst{typ: "aws_secrets_manager"}) + + c.InvalidateAll() + + if c.Size() != 0 { + t.Fatalf("InvalidateAll: size %d, want 0", c.Size()) + } +} diff --git a/apps/workspace-engine/pkg/secrets/resolver.go b/apps/workspace-engine/pkg/secrets/resolver.go index 6316cd53f..dc877858f 100644 --- a/apps/workspace-engine/pkg/secrets/resolver.go +++ b/apps/workspace-engine/pkg/secrets/resolver.go @@ -8,27 +8,41 @@ import ( ) // Resolver glues the ProviderConfigStore (lookup + decrypt), the Registry -// (factory dispatch), and the Cache (TTL'd resolved values). One Resolver is +// (factory dispatch), the value cache (TTL'd resolved plaintexts), and the +// provider cache (TTL'd constructed Provider instances). One Resolver is // constructed at startup and shared by all reconciliation goroutines. type Resolver struct { - store ProviderConfigStore - registry *Registry - cache *Cache + store ProviderConfigStore + registry *Registry + cache *Cache + providerCache *ProviderCache } -// NewResolver builds a Resolver. A nil cache disables caching. -func NewResolver(store ProviderConfigStore, registry *Registry, cache *Cache) *Resolver { - return &Resolver{store: store, registry: registry, cache: cache} +// NewResolver builds a Resolver. A nil cache or providerCache disables that +// layer of caching while leaving the rest of the lookup chain intact. +func NewResolver( + store ProviderConfigStore, + registry *Registry, + cache *Cache, + providerCache *ProviderCache, +) *Resolver { + return &Resolver{ + store: store, + registry: registry, + cache: cache, + providerCache: providerCache, + } } // Resolve fetches the secret value identified by ref. Lookups proceed: // -// 1. cache (if configured) -// 2. ProviderConfigStore.Get to load + decrypt the provider config -// 3. Registry.Build to construct a Provider from the config -// 4. Provider.Resolve to hit the upstream secret store +// 1. value cache (if configured) +// 2. provider-instance cache (skips store + factory on hit) +// 3. ProviderConfigStore.Get to load + decrypt the provider config +// 4. Registry.Build to construct a Provider from the config +// 5. Provider.Resolve to hit the upstream secret store // -// Any error in steps 2-4 propagates; release dispatch is expected to block. +// Any error in steps 3-5 propagates; release dispatch is expected to block. func (r *Resolver) Resolve( ctx context.Context, workspaceID uuid.UUID, @@ -47,19 +61,19 @@ func (r *Resolver) Resolve( } } - cfg, err := r.store.Get(ctx, workspaceID, ref.Provider) - if err != nil { - return "", err - } - - provider, err := r.registry.Build(cfg) + provider, providerType, err := r.lookupProvider(ctx, workspaceID, ref.Provider) if err != nil { return "", err } value, err := provider.Resolve(ctx, ref) if err != nil { - return "", fmt.Errorf("secrets: provider %q (%s) resolve: %w", cfg.Name, cfg.Type, err) + return "", fmt.Errorf( + "secrets: provider %q (%s) resolve: %w", + ref.Provider, + providerType, + err, + ) } if r.cache != nil { @@ -68,10 +82,46 @@ func (r *Resolver) Resolve( return value, nil } -// InvalidateProvider drops cached entries for the named provider. Wire to the -// LISTEN/NOTIFY consumer on the `secret_provider_invalidate` channel. +// lookupProvider returns the Provider for the named secret_provider row in +// the workspace. The provider-instance cache is checked first; on a miss the +// config is loaded + decrypted via the store and constructed via the +// registry, then memoized. +func (r *Resolver) lookupProvider( + ctx context.Context, + workspaceID uuid.UUID, + providerName string, +) (Provider, string, error) { + if r.providerCache != nil { + if p, ok := r.providerCache.Get(workspaceID, providerName); ok { + return p, p.Type(), nil + } + } + + cfg, err := r.store.Get(ctx, workspaceID, providerName) + if err != nil { + return nil, "", err + } + + provider, err := r.registry.Build(cfg) + if err != nil { + return nil, "", err + } + + if r.providerCache != nil { + r.providerCache.Set(workspaceID, providerName, provider) + } + return provider, cfg.Type, nil +} + +// InvalidateProvider drops cached resolved values and the cached Provider +// instance for the named provider. Wire to the LISTEN/NOTIFY consumer on +// the `secret_provider_invalidate` channel so an api-side update flushes +// every workspace-engine pod. func (r *Resolver) InvalidateProvider(workspaceID uuid.UUID, providerName string) { if r.cache != nil { r.cache.InvalidateProvider(workspaceID, providerName) } + if r.providerCache != nil { + r.providerCache.Invalidate(workspaceID, providerName) + } } diff --git a/apps/workspace-engine/pkg/secrets/resolver_test.go b/apps/workspace-engine/pkg/secrets/resolver_test.go index a98cc3fe5..f4fd09e6a 100644 --- a/apps/workspace-engine/pkg/secrets/resolver_test.go +++ b/apps/workspace-engine/pkg/secrets/resolver_test.go @@ -70,7 +70,7 @@ func newResolver(t *testing.T, store ProviderConfigStore, provider Provider) *Re provider.Type(), func(_ json.RawMessage) (Provider, error) { return provider, nil }, ) - return NewResolver(store, reg, NewCache(time.Minute)) + return NewResolver(store, reg, NewCache(time.Minute), NewProviderCache(time.Minute)) } func TestResolverHappyPath(t *testing.T) { @@ -164,6 +164,130 @@ func TestResolverInvalidationForcesRefetch(t *testing.T) { } } +func TestResolverProviderInstanceCachedAcrossRefs(t *testing.T) { + // Two distinct SecretReferences sharing a provider should construct the + // Provider once. With only the value cache (and no provider cache), + // every distinct ref would re-decrypt and re-build. + ws := uuid.New() + store := &mockStore{ + configs: map[string]*ProviderConfig{ + "aws-prod": { + WorkspaceID: ws, + Name: "aws-prod", + Type: "aws_secrets_manager", + Config: json.RawMessage(`{}`), + }, + }, + } + provider := &mockProvider{t: "aws_secrets_manager", resolveVal: "x"} + factoryCalls := 0 + reg := NewRegistry() + reg.Register("aws_secrets_manager", func(_ json.RawMessage) (Provider, error) { + factoryCalls++ + return provider, nil + }) + r := NewResolver(store, reg, NewCache(time.Minute), NewProviderCache(time.Minute)) + + refA := SecretReference{Provider: "aws-prod", Path: "prod/db", Key: "password"} + refB := SecretReference{Provider: "aws-prod", Path: "prod/db", Key: "username"} + if _, err := r.Resolve(context.Background(), ws, refA); err != nil { + t.Fatalf("Resolve A: %v", err) + } + if _, err := r.Resolve(context.Background(), ws, refB); err != nil { + t.Fatalf("Resolve B: %v", err) + } + + if got := store.getCalls.Load(); got != 1 { + t.Fatalf("store.Get called %d times, want 1 (second ref should reuse cached provider)", got) + } + if factoryCalls != 1 { + t.Fatalf("factory called %d times, want 1", factoryCalls) + } + if got := len(provider.resolveRefs); got != 2 { + t.Fatalf("provider.Resolve called %d times, want 2 (one per distinct value ref)", got) + } +} + +func TestResolverInvalidationDropsProviderInstance(t *testing.T) { + ws := uuid.New() + store := &mockStore{ + configs: map[string]*ProviderConfig{ + "aws-prod": { + WorkspaceID: ws, + Name: "aws-prod", + Type: "aws_secrets_manager", + Config: json.RawMessage(`{}`), + }, + }, + } + provider := &mockProvider{t: "aws_secrets_manager", resolveVal: "x"} + factoryCalls := 0 + reg := NewRegistry() + reg.Register("aws_secrets_manager", func(_ json.RawMessage) (Provider, error) { + factoryCalls++ + return provider, nil + }) + r := NewResolver(store, reg, NewCache(time.Minute), NewProviderCache(time.Minute)) + + ref := SecretReference{Provider: "aws-prod", Path: "prod/db", Key: "password"} + if _, err := r.Resolve(context.Background(), ws, ref); err != nil { + t.Fatalf("Resolve: %v", err) + } + r.InvalidateProvider(ws, "aws-prod") + if _, err := r.Resolve(context.Background(), ws, ref); err != nil { + t.Fatalf("Resolve after invalidation: %v", err) + } + + if got := store.getCalls.Load(); got != 2 { + t.Fatalf( + "store.Get called %d times, want 2 (invalidation forces re-decrypt)", + got, + ) + } + if factoryCalls != 2 { + t.Fatalf("factory called %d times, want 2 after invalidation", factoryCalls) + } +} + +func TestResolverProviderCacheDisabledWhenNil(t *testing.T) { + ws := uuid.New() + store := &mockStore{ + configs: map[string]*ProviderConfig{ + "aws-prod": { + WorkspaceID: ws, + Name: "aws-prod", + Type: "aws_secrets_manager", + Config: json.RawMessage(`{}`), + }, + }, + } + provider := &mockProvider{t: "aws_secrets_manager", resolveVal: "x"} + factoryCalls := 0 + reg := NewRegistry() + reg.Register("aws_secrets_manager", func(_ json.RawMessage) (Provider, error) { + factoryCalls++ + return provider, nil + }) + // Disable both caches: every Resolve must hit store + factory. + r := NewResolver(store, reg, nil, nil) + + refA := SecretReference{Provider: "aws-prod", Path: "prod/db", Key: "password"} + refB := SecretReference{Provider: "aws-prod", Path: "prod/db", Key: "username"} + if _, err := r.Resolve(context.Background(), ws, refA); err != nil { + t.Fatalf("Resolve A: %v", err) + } + if _, err := r.Resolve(context.Background(), ws, refB); err != nil { + t.Fatalf("Resolve B: %v", err) + } + + if got := store.getCalls.Load(); got != 2 { + t.Fatalf("store.Get called %d times, want 2 with caches off", got) + } + if factoryCalls != 2 { + t.Fatalf("factory called %d times, want 2 with caches off", factoryCalls) + } +} + func TestResolverRejectsEmptyRefFields(t *testing.T) { store := &mockStore{} provider := &mockProvider{t: "doppler"} @@ -195,7 +319,7 @@ func TestResolverNoFactoryRegistered(t *testing.T) { }, }, } - r := NewResolver(store, NewRegistry(), NewCache(time.Minute)) + r := NewResolver(store, NewRegistry(), NewCache(time.Minute), NewProviderCache(time.Minute)) _, err := r.Resolve(context.Background(), ws, SecretReference{Provider: "unknown", Key: "K"}) if err == nil { From 54dd28a4b64ad5409f2021ce20dfee2d3d89bb64 Mon Sep 17 00:00:00 2001 From: Michael Leone Date: Thu, 14 May 2026 08:54:37 -0400 Subject: [PATCH 09/13] feat(jobs): template-render JobAgentConfig against the dispatch context Job agent configs now interpolate {{ . }} expressions at dispatch time so secrets resolved into dc.JobAgentVariables (and any other DispatchContext field) can be referenced directly from agent config fields like apiKey, token, or password. Without this, those fields were opaque strings and the agent saw the literal "{{ .jobAgentVariables.argo_token }}" instead of the resolved value. - pkg/workspace/jobs/config_template.go: renderJobAgentConfig walks the oapi.JobAgentConfig map, recurses into nested maps and arrays, and runs every string value that contains "{{" through pkg/templatefuncs against dispatchCtx.Map(). Strings without "{{" pass through unchanged; non-string scalars (numbers, booleans, nil) are untouched. Errors include the dotted path of the offending field for fast operator triage. - pkg/workspace/jobs/factory.go: BuildDispatchContext calls renderJobAgentConfig immediately after populateJobAgentVariables, so the agent receives a JobAgentConfig with secrets already substituted. The typed agent config readers (GetArgoCDJobAgentConfig etc.) read the rendered values without any agent-side changes. - Template render is a pure in-memory substitution: it does not call the secret provider. Provider calls happen earlier, inside populateJobAgentVariables -> variableresolver.ResolveForJobAgent -> secrets.Resolver.Resolve. The render only reuses what's already in dc.JobAgentVariables (and dc.Release.Variables, dc.Resource, etc.). - Tests cover string passthrough, nested map / array recursion, scalar passthrough, parse-error reporting with field path, empty-config short circuit, missing top-level key error (security-safe: no silent empty), and visible "" marker for a missing leaf key. --- apps/workspace-engine/main.go | 4 +- apps/workspace-engine/oapi/openapi.json | 7 + .../oapi/spec/schemas/jobs.jsonnet | 5 + apps/workspace-engine/pkg/db/convert.go | 17 ++ .../pkg/db/queries/variables.sql | 36 ++++ apps/workspace-engine/pkg/db/variables.sql.go | 79 ++++++++ apps/workspace-engine/pkg/oapi/oapi.gen.go | 23 +-- apps/workspace-engine/pkg/secrets/resolver.go | 111 ++++++++++-- .../pkg/secrets/resolver_test.go | 19 +- .../pkg/workspace/jobs/config_template.go | 100 +++++++++++ .../workspace/jobs/config_template_test.go | 168 ++++++++++++++++++ .../pkg/workspace/jobs/factory.go | 82 ++++++++- .../desiredrelease/reconcile_test.go | 7 + .../variableresolver/getters.go | 4 + .../variableresolver/getters_postgres.go | 35 ++++ .../variableresolver/job_agent.go | 114 ++++++++++++ .../variableresolver/resolve_test.go | 9 + .../svc/controllers/forcedeploy/controller.go | 27 ++- .../svc/controllers/forcedeploy/reconcile.go | 6 +- .../controllers/forcedeploy/reconcile_test.go | 12 +- .../controllers/jobeligibility/controller.go | 25 ++- .../controllers/jobeligibility/reconcile.go | 21 ++- .../jobeligibility/reconcile_test.go | 125 ++++++------- .../test/controllers/harness/mocks.go | 7 + 24 files changed, 916 insertions(+), 127 deletions(-) create mode 100644 apps/workspace-engine/pkg/workspace/jobs/config_template.go create mode 100644 apps/workspace-engine/pkg/workspace/jobs/config_template_test.go create mode 100644 apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/job_agent.go diff --git a/apps/workspace-engine/main.go b/apps/workspace-engine/main.go index 3f228528f..b74d513a3 100644 --- a/apps/workspace-engine/main.go +++ b/apps/workspace-engine/main.go @@ -62,9 +62,9 @@ func main() { deploymentplanresult.New(WorkerID, db.GetPool(ctx)), deploymentresourceselectoreval.New(WorkerID, db.GetPool(ctx)), environmentresourceselectoreval.New(WorkerID, db.GetPool(ctx)), - forcedeploy.New(WorkerID, db.GetPool(ctx)), + forcedeploy.New(WorkerID, db.GetPool(ctx), secretResolver), jobdispatch.New(WorkerID, db.GetPool(ctx)), - jobeligibility.New(WorkerID, db.GetPool(ctx)), + jobeligibility.New(WorkerID, db.GetPool(ctx), secretResolver), jobverificationmetric.New(WorkerID, db.GetPool(ctx)), relationshipeval.New(WorkerID, db.GetPool(ctx)), desiredrelease.New(WorkerID, db.GetPool(ctx), secretResolver), diff --git a/apps/workspace-engine/oapi/openapi.json b/apps/workspace-engine/oapi/openapi.json index 967b1f033..6e82acc61 100644 --- a/apps/workspace-engine/oapi/openapi.json +++ b/apps/workspace-engine/oapi/openapi.json @@ -495,6 +495,13 @@ "jobAgentConfig": { "$ref": "#/components/schemas/JobAgentConfig" }, + "jobAgentVariables": { + "additionalProperties": { + "$ref": "#/components/schemas/LiteralValue" + }, + "description": "Variables scoped to the dispatching job agent. Resolved at dispatch time and referenced from agent-config templates as {{ .jobAgentVariables. }}.", + "type": "object" + }, "release": { "$ref": "#/components/schemas/Release" }, diff --git a/apps/workspace-engine/oapi/spec/schemas/jobs.jsonnet b/apps/workspace-engine/oapi/spec/schemas/jobs.jsonnet index 667b1fe1b..5870773c9 100644 --- a/apps/workspace-engine/oapi/spec/schemas/jobs.jsonnet +++ b/apps/workspace-engine/oapi/spec/schemas/jobs.jsonnet @@ -66,6 +66,11 @@ local JobPropertyKeys = std.objectFields(Job.properties); type: 'object', additionalProperties: openapi.schemaRef('LiteralValue'), }, + jobAgentVariables: { + type: 'object', + additionalProperties: openapi.schemaRef('LiteralValue'), + description: 'Variables scoped to the dispatching job agent. Resolved at dispatch time and referenced from agent-config templates as {{ .jobAgentVariables. }}.', + }, }, }, diff --git a/apps/workspace-engine/pkg/db/convert.go b/apps/workspace-engine/pkg/db/convert.go index a93bde8f2..5fa6fa553 100644 --- a/apps/workspace-engine/pkg/db/convert.go +++ b/apps/workspace-engine/pkg/db/convert.go @@ -362,6 +362,23 @@ func ToOapiDeploymentVariable( return v } +// ToOapiJobAgentVariable maps a job_agent-scoped variable row to the same +// oapi.DeploymentVariable shape used by deployment variables, so the +// variableresolver can run a single resolution pipeline regardless of scope. +// DeploymentId is left empty since the variable does not belong to one. +func ToOapiJobAgentVariable( + row ListVariablesWithValuesByJobAgentIDRow, +) oapi.DeploymentVariable { + v := oapi.DeploymentVariable{ + Id: row.ID.String(), + Key: row.Key, + } + if row.Description.Valid { + v.Description = &row.Description.String + } + return v +} + func ToOapiDeploymentVariableValueFromAgg( r VariableValueAggRow, ) (oapi.DeploymentVariableValue, error) { diff --git a/apps/workspace-engine/pkg/db/queries/variables.sql b/apps/workspace-engine/pkg/db/queries/variables.sql index 3d26b412b..701343f9f 100644 --- a/apps/workspace-engine/pkg/db/queries/variables.sql +++ b/apps/workspace-engine/pkg/db/queries/variables.sql @@ -103,3 +103,39 @@ SELECT FROM variable v INNER JOIN resource r ON r.id = v.resource_id WHERE v.scope = 'resource' AND r.workspace_id = $1 AND r.deleted_at IS NULL; + +-- name: ListVariablesWithValuesByJobAgentID :many +SELECT + v.id, + v.scope, + v.deployment_id, + v.resource_id, + v.job_agent_id, + v.key, + v.is_sensitive, + v.description, + COALESCE( + ( + SELECT json_agg( + json_build_object( + 'id', vv.id, + 'variableId', vv.variable_id, + 'resourceSelector', vv.resource_selector, + 'priority', vv.priority, + 'kind', vv.kind, + 'literalValue', vv.literal_value, + 'refKey', vv.ref_key, + 'refPath', vv.ref_path, + 'secretProvider', vv.secret_provider, + 'secretKey', vv.secret_key, + 'secretPath', vv.secret_path + ) + ORDER BY vv.priority DESC, vv.id ASC + ) + FROM variable_value vv + WHERE vv.variable_id = v.id + ), + '[]'::json + ) AS values +FROM variable v +WHERE v.scope = 'job_agent' AND v.job_agent_id = $1; diff --git a/apps/workspace-engine/pkg/db/variables.sql.go b/apps/workspace-engine/pkg/db/variables.sql.go index 0d170f890..867c80f6e 100644 --- a/apps/workspace-engine/pkg/db/variables.sql.go +++ b/apps/workspace-engine/pkg/db/variables.sql.go @@ -162,6 +162,85 @@ func (q *Queries) ListVariablesWithValuesByDeploymentID(ctx context.Context, dep return items, nil } +const listVariablesWithValuesByJobAgentID = `-- name: ListVariablesWithValuesByJobAgentID :many +SELECT + v.id, + v.scope, + v.deployment_id, + v.resource_id, + v.job_agent_id, + v.key, + v.is_sensitive, + v.description, + COALESCE( + ( + SELECT json_agg( + json_build_object( + 'id', vv.id, + 'variableId', vv.variable_id, + 'resourceSelector', vv.resource_selector, + 'priority', vv.priority, + 'kind', vv.kind, + 'literalValue', vv.literal_value, + 'refKey', vv.ref_key, + 'refPath', vv.ref_path, + 'secretProvider', vv.secret_provider, + 'secretKey', vv.secret_key, + 'secretPath', vv.secret_path + ) + ORDER BY vv.priority DESC, vv.id ASC + ) + FROM variable_value vv + WHERE vv.variable_id = v.id + ), + '[]'::json + ) AS values +FROM variable v +WHERE v.scope = 'job_agent' AND v.job_agent_id = $1 +` + +type ListVariablesWithValuesByJobAgentIDRow struct { + ID uuid.UUID + Scope VariableScope + DeploymentID uuid.UUID + ResourceID uuid.UUID + JobAgentID uuid.UUID + Key string + IsSensitive bool + Description pgtype.Text + Values []byte +} + +func (q *Queries) ListVariablesWithValuesByJobAgentID(ctx context.Context, jobAgentID uuid.UUID) ([]ListVariablesWithValuesByJobAgentIDRow, error) { + rows, err := q.db.Query(ctx, listVariablesWithValuesByJobAgentID, jobAgentID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListVariablesWithValuesByJobAgentIDRow + for rows.Next() { + var i ListVariablesWithValuesByJobAgentIDRow + if err := rows.Scan( + &i.ID, + &i.Scope, + &i.DeploymentID, + &i.ResourceID, + &i.JobAgentID, + &i.Key, + &i.IsSensitive, + &i.Description, + &i.Values, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const listVariablesWithValuesByResourceID = `-- name: ListVariablesWithValuesByResourceID :many SELECT v.id, diff --git a/apps/workspace-engine/pkg/oapi/oapi.gen.go b/apps/workspace-engine/pkg/oapi/oapi.gen.go index 101cdf01d..42aee3024 100644 --- a/apps/workspace-engine/pkg/oapi/oapi.gen.go +++ b/apps/workspace-engine/pkg/oapi/oapi.gen.go @@ -400,16 +400,19 @@ type DispatchContext struct { Environment *Environment `json:"environment,omitempty"` // Inputs Resolved input values for the workflow run. - Inputs *map[string]interface{} `json:"inputs,omitempty"` - JobAgent JobAgent `json:"jobAgent"` - JobAgentConfig JobAgentConfig `json:"jobAgentConfig"` - Release *Release `json:"release,omitempty"` - Resource *Resource `json:"resource,omitempty"` - Variables *map[string]LiteralValue `json:"variables,omitempty"` - Version *DeploymentVersion `json:"version,omitempty"` - Workflow *Workflow `json:"workflow,omitempty"` - WorkflowJob *WorkflowJob `json:"workflowJob,omitempty"` - WorkflowRun *WorkflowRun `json:"workflowRun,omitempty"` + Inputs *map[string]interface{} `json:"inputs,omitempty"` + JobAgent JobAgent `json:"jobAgent"` + JobAgentConfig JobAgentConfig `json:"jobAgentConfig"` + + // JobAgentVariables Variables scoped to the dispatching job agent. Resolved at dispatch time and referenced from agent-config templates as {{ .jobAgentVariables. }}. + JobAgentVariables *map[string]LiteralValue `json:"jobAgentVariables,omitempty"` + Release *Release `json:"release,omitempty"` + Resource *Resource `json:"resource,omitempty"` + Variables *map[string]LiteralValue `json:"variables,omitempty"` + Version *DeploymentVersion `json:"version,omitempty"` + Workflow *Workflow `json:"workflow,omitempty"` + WorkflowJob *WorkflowJob `json:"workflowJob,omitempty"` + WorkflowRun *WorkflowRun `json:"workflowRun,omitempty"` } // EntityRelation defines model for EntityRelation. diff --git a/apps/workspace-engine/pkg/secrets/resolver.go b/apps/workspace-engine/pkg/secrets/resolver.go index dc877858f..d5b8cd94c 100644 --- a/apps/workspace-engine/pkg/secrets/resolver.go +++ b/apps/workspace-engine/pkg/secrets/resolver.go @@ -3,10 +3,17 @@ package secrets import ( "context" "fmt" + "log/slog" "github.com/google/uuid" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" ) +var tracer = otel.Tracer("workspace-engine/pkg/secrets") + // Resolver glues the ProviderConfigStore (lookup + decrypt), the Registry // (factory dispatch), the value cache (TTL'd resolved plaintexts), and the // provider cache (TTL'd constructed Provider instances). One Resolver is @@ -43,74 +50,154 @@ func NewResolver( // 5. Provider.Resolve to hit the upstream secret store // // Any error in steps 3-5 propagates; release dispatch is expected to block. +// +// Observability: every call opens a "secrets.Resolve" span with non-sensitive +// reference metadata (provider/path/key — never the plaintext) and emits a +// structured slog record on each terminal outcome. Cache hits are recorded as +// span events so traces show which layer absorbed the call. func (r *Resolver) Resolve( ctx context.Context, workspaceID uuid.UUID, ref SecretReference, ) (string, error) { + ctx, span := tracer.Start(ctx, "secrets.Resolve") + defer span.End() + span.SetAttributes( + attribute.String("workspace.id", workspaceID.String()), + attribute.String("secret.provider", ref.Provider), + attribute.String("secret.path", ref.Path), + attribute.String("secret.key", ref.Key), + ) + if ref.Provider == "" { - return "", fmt.Errorf("secrets: empty provider name in reference") - } - if ref.Key == "" { - return "", fmt.Errorf("secrets: empty key in reference") + err := fmt.Errorf("secrets: empty provider name in reference") + r.recordFailure(ctx, span, workspaceID, ref, "", "validation", err) + return "", err } + // Per-provider Key semantics are validated by the Provider impl: awssm + // treats an empty Key as "return the raw SecretString", while Doppler + // and env require a non-empty Key. if r.cache != nil { if v, ok := r.cache.Get(workspaceID, ref); ok { + span.AddEvent("value_cache.hit") + span.SetAttributes( + attribute.Bool("secret.value_cache_hit", true), + attribute.Int("secret.value.length", len(v)), + ) + slog.DebugContext(ctx, "secret resolved (value cache hit)", + "workspace_id", workspaceID.String(), + "provider", ref.Provider, + "path", ref.Path, + "key", ref.Key, + ) return v, nil } } - provider, providerType, err := r.lookupProvider(ctx, workspaceID, ref.Provider) + provider, providerType, providerCacheHit, err := r.lookupProvider( + ctx, + workspaceID, + ref.Provider, + ) if err != nil { + r.recordFailure(ctx, span, workspaceID, ref, "", "provider_lookup", err) return "", err } + span.SetAttributes( + attribute.String("secret.provider_type", providerType), + attribute.Bool("secret.provider_cache_hit", providerCacheHit), + ) + if providerCacheHit { + span.AddEvent("provider_cache.hit") + } value, err := provider.Resolve(ctx, ref) if err != nil { - return "", fmt.Errorf( + wrapped := fmt.Errorf( "secrets: provider %q (%s) resolve: %w", ref.Provider, providerType, err, ) + r.recordFailure(ctx, span, workspaceID, ref, providerType, "upstream", wrapped) + return "", wrapped } if r.cache != nil { r.cache.Set(workspaceID, ref, value) } + + span.SetAttributes(attribute.Int("secret.value.length", len(value))) + span.SetStatus(codes.Ok, "") + slog.InfoContext(ctx, "secret resolved", + "workspace_id", workspaceID.String(), + "provider", ref.Provider, + "provider_type", providerType, + "path", ref.Path, + "key", ref.Key, + "provider_cache_hit", providerCacheHit, + "value_length", len(value), + ) return value, nil } +// recordFailure attaches the error to the span and emits a structured +// warning log. The plaintext is never recorded; only the reference metadata +// and a coarse error class. +func (r *Resolver) recordFailure( + ctx context.Context, + span trace.Span, + workspaceID uuid.UUID, + ref SecretReference, + providerType string, + errorClass string, + err error, +) { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + span.SetAttributes(attribute.String("secret.error_class", errorClass)) + slog.WarnContext(ctx, "secret resolve failed", + "workspace_id", workspaceID.String(), + "provider", ref.Provider, + "provider_type", providerType, + "path", ref.Path, + "key", ref.Key, + "error_class", errorClass, + "error", err.Error(), + ) +} + // lookupProvider returns the Provider for the named secret_provider row in // the workspace. The provider-instance cache is checked first; on a miss the // config is loaded + decrypted via the store and constructed via the -// registry, then memoized. +// registry, then memoized. The boolean indicates whether the result came +// from the cache. func (r *Resolver) lookupProvider( ctx context.Context, workspaceID uuid.UUID, providerName string, -) (Provider, string, error) { +) (Provider, string, bool, error) { if r.providerCache != nil { if p, ok := r.providerCache.Get(workspaceID, providerName); ok { - return p, p.Type(), nil + return p, p.Type(), true, nil } } cfg, err := r.store.Get(ctx, workspaceID, providerName) if err != nil { - return nil, "", err + return nil, "", false, err } provider, err := r.registry.Build(cfg) if err != nil { - return nil, "", err + return nil, "", false, err } if r.providerCache != nil { r.providerCache.Set(workspaceID, providerName, provider) } - return provider, cfg.Type, nil + return provider, cfg.Type, false, nil } // InvalidateProvider drops cached resolved values and the cached Provider diff --git a/apps/workspace-engine/pkg/secrets/resolver_test.go b/apps/workspace-engine/pkg/secrets/resolver_test.go index f4fd09e6a..3c4a9b44f 100644 --- a/apps/workspace-engine/pkg/secrets/resolver_test.go +++ b/apps/workspace-engine/pkg/secrets/resolver_test.go @@ -288,19 +288,20 @@ func TestResolverProviderCacheDisabledWhenNil(t *testing.T) { } } -func TestResolverRejectsEmptyRefFields(t *testing.T) { +func TestResolverRejectsEmptyProvider(t *testing.T) { + // Empty Provider is always invalid — there's nothing to look up. Empty + // Key is provider-specific (awssm treats it as "return raw") and is + // validated inside the Provider impl, not at the Resolver level. store := &mockStore{} provider := &mockProvider{t: "doppler"} r := newResolver(t, store, provider) - cases := []SecretReference{ - {Provider: "", Key: "K"}, - {Provider: "p", Key: ""}, - } - for _, c := range cases { - if _, err := r.Resolve(context.Background(), uuid.New(), c); err == nil { - t.Fatalf("expected error for ref %+v", c) - } + if _, err := r.Resolve( + context.Background(), + uuid.New(), + SecretReference{Provider: "", Key: "K"}, + ); err == nil { + t.Fatal("expected error for empty Provider") } if got := store.getCalls.Load(); got != 0 { t.Fatalf("store should not be hit for invalid refs, got %d calls", got) diff --git a/apps/workspace-engine/pkg/workspace/jobs/config_template.go b/apps/workspace-engine/pkg/workspace/jobs/config_template.go new file mode 100644 index 000000000..47adac5c6 --- /dev/null +++ b/apps/workspace-engine/pkg/workspace/jobs/config_template.go @@ -0,0 +1,100 @@ +package jobs + +import ( + "bytes" + "fmt" + "strings" + + "workspace-engine/pkg/oapi" + "workspace-engine/pkg/templatefuncs" +) + +// renderJobAgentConfig walks the job agent config and renders every string +// value that contains a Go template directive (`{{`) against the dispatch +// context. Maps and slices are recursed into. Non-string scalars (numbers, +// booleans, nil) pass through unchanged. The original config is left +// untouched; a new map is returned so the job agent receives a config with +// secrets already resolved. +// +// Strings that do not contain `{{` are returned verbatim — this avoids +// surprising changes for configs that legitimately include double braces in +// non-template content, and lets template render failures surface only when +// the operator intentionally used template syntax. +func renderJobAgentConfig( + cfg oapi.JobAgentConfig, + dispatchCtx *oapi.DispatchContext, +) (oapi.JobAgentConfig, error) { + if len(cfg) == 0 { + return cfg, nil + } + data := dispatchCtx.Map() + // oapi.JobAgentConfig is a named map type; the type switch in + // renderValue matches map[string]any literally, so convert to the + // unnamed form before recursing. + asMap := map[string]any(cfg) + rendered, err := renderValue(asMap, data, "") + if err != nil { + return nil, err + } + out, ok := rendered.(map[string]any) + if !ok { + return nil, fmt.Errorf("job agent config: rendered value is not a map") + } + return oapi.JobAgentConfig(out), nil +} + +// renderValue walks any JSON-shaped value (string / number / bool / nil / +// []any / map[string]any) and renders string leaves containing `{{`. The +// path argument is used to label template parse / execute errors. +func renderValue(v any, data map[string]any, path string) (any, error) { + switch t := v.(type) { + case string: + if !strings.Contains(t, "{{") { + return t, nil + } + return renderString(t, data, path) + case map[string]any: + out := make(map[string]any, len(t)) + for k, child := range t { + childPath := joinPath(path, k) + rendered, err := renderValue(child, data, childPath) + if err != nil { + return nil, err + } + out[k] = rendered + } + return out, nil + case []any: + out := make([]any, len(t)) + for i, child := range t { + childPath := fmt.Sprintf("%s[%d]", path, i) + rendered, err := renderValue(child, data, childPath) + if err != nil { + return nil, err + } + out[i] = rendered + } + return out, nil + default: + return v, nil + } +} + +func renderString(tmpl string, data map[string]any, path string) (string, error) { + t, err := templatefuncs.Parse("jobAgentConfig:"+path, tmpl) + if err != nil { + return "", fmt.Errorf("job agent config %q: parse template: %w", path, err) + } + var buf bytes.Buffer + if err := t.Execute(&buf, data); err != nil { + return "", fmt.Errorf("job agent config %q: execute template: %w", path, err) + } + return buf.String(), nil +} + +func joinPath(path, key string) string { + if path == "" { + return key + } + return path + "." + key +} diff --git a/apps/workspace-engine/pkg/workspace/jobs/config_template_test.go b/apps/workspace-engine/pkg/workspace/jobs/config_template_test.go new file mode 100644 index 000000000..4a85dd09f --- /dev/null +++ b/apps/workspace-engine/pkg/workspace/jobs/config_template_test.go @@ -0,0 +1,168 @@ +package jobs + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "workspace-engine/pkg/oapi" +) + +func dispatchCtxWithJobAgentVars(vars map[string]string) *oapi.DispatchContext { + dc := &oapi.DispatchContext{} + if vars != nil { + m := make(map[string]oapi.LiteralValue, len(vars)) + for k, v := range vars { + m[k] = *oapi.NewLiteralValue(v) + } + dc.JobAgentVariables = &m + } + return dc +} + +func TestRenderJobAgentConfig_RendersStringTemplates(t *testing.T) { + dc := dispatchCtxWithJobAgentVars(map[string]string{ + "argo_token": "supersecret123", + }) + cfg := oapi.JobAgentConfig{ + "serverUrl": "https://argocd.example.com", + "apiKey": "{{ .jobAgentVariables.argo_token }}", + } + + out, err := renderJobAgentConfig(cfg, dc) + require.NoError(t, err) + assert.Equal(t, "https://argocd.example.com", out["serverUrl"]) + assert.Equal(t, "supersecret123", out["apiKey"]) +} + +func TestRenderJobAgentConfig_LeavesPlainStringsAlone(t *testing.T) { + dc := dispatchCtxWithJobAgentVars(nil) + cfg := oapi.JobAgentConfig{ + "serverUrl": "https://argocd.example.com", + "template": "no braces here", + } + + out, err := renderJobAgentConfig(cfg, dc) + require.NoError(t, err) + assert.Equal(t, "https://argocd.example.com", out["serverUrl"]) + assert.Equal(t, "no braces here", out["template"]) +} + +func TestRenderJobAgentConfig_RecursesIntoNestedMaps(t *testing.T) { + dc := dispatchCtxWithJobAgentVars(map[string]string{ + "region": "us-east-1", + "token": "abc123", + }) + cfg := oapi.JobAgentConfig{ + "aws": map[string]any{ + "region": "{{ .jobAgentVariables.region }}", + "credentials": map[string]any{"token": "{{ .jobAgentVariables.token }}"}, + }, + } + + out, err := renderJobAgentConfig(cfg, dc) + require.NoError(t, err) + aws := out["aws"].(map[string]any) + assert.Equal(t, "us-east-1", aws["region"]) + creds := aws["credentials"].(map[string]any) + assert.Equal(t, "abc123", creds["token"]) +} + +func TestRenderJobAgentConfig_RecursesIntoArrays(t *testing.T) { + dc := dispatchCtxWithJobAgentVars(map[string]string{ + "a": "first", + "b": "second", + }) + cfg := oapi.JobAgentConfig{ + "hosts": []any{ + "{{ .jobAgentVariables.a }}", + "{{ .jobAgentVariables.b }}", + "literal", + }, + } + + out, err := renderJobAgentConfig(cfg, dc) + require.NoError(t, err) + hosts := out["hosts"].([]any) + assert.Equal(t, []any{"first", "second", "literal"}, hosts) +} + +func TestRenderJobAgentConfig_NonStringScalarsPassThrough(t *testing.T) { + dc := dispatchCtxWithJobAgentVars(nil) + cfg := oapi.JobAgentConfig{ + "timeoutSeconds": 30, + "insecure": false, + "nullField": nil, + } + + out, err := renderJobAgentConfig(cfg, dc) + require.NoError(t, err) + assert.Equal(t, 30, out["timeoutSeconds"]) + assert.Equal(t, false, out["insecure"]) + assert.Nil(t, out["nullField"]) +} + +func TestRenderJobAgentConfig_MissingKeyErrors(t *testing.T) { + // templatefuncs.New applies Option("missingkey=zero"): the missing + // top-level "jobAgentVariables" key renders to zero (nil interface), + // and traversing into it (.unknown) raises a nil-pointer error. The + // operator gets a clear template error instead of an empty string + // silently propagating into the agent's config. + dc := dispatchCtxWithJobAgentVars(nil) + cfg := oapi.JobAgentConfig{ + "apiKey": "{{ .jobAgentVariables.unknown }}", + } + + _, err := renderJobAgentConfig(cfg, dc) + require.Error(t, err) + assert.Contains(t, err.Error(), "apiKey") +} + +func TestRenderJobAgentConfig_MissingLeafKey(t *testing.T) { + // When jobAgentVariables exists but the leaf key does not, missingkey=zero + // returns nil for map[string]any element type, which Go's text/template + // prints as "". Operators get a visible marker rather than a + // silent empty string, which is the safer default for security config. + dc := dispatchCtxWithJobAgentVars(map[string]string{"present": "x"}) + cfg := oapi.JobAgentConfig{ + "apiKey": "{{ .jobAgentVariables.absent }}", + } + + out, err := renderJobAgentConfig(cfg, dc) + require.NoError(t, err) + assert.Equal(t, "", out["apiKey"]) +} + +func TestRenderJobAgentConfig_ParseError(t *testing.T) { + dc := dispatchCtxWithJobAgentVars(nil) + cfg := oapi.JobAgentConfig{ + "apiKey": "{{ .unterminated", + } + + _, err := renderJobAgentConfig(cfg, dc) + require.Error(t, err) + assert.Contains(t, err.Error(), "parse template") + assert.Contains(t, err.Error(), "apiKey") +} + +func TestRenderJobAgentConfig_EmptyConfigUnchanged(t *testing.T) { + dc := dispatchCtxWithJobAgentVars(nil) + out, err := renderJobAgentConfig(oapi.JobAgentConfig{}, dc) + require.NoError(t, err) + assert.Empty(t, out) +} + +func TestRenderJobAgentConfig_DispatchContextFieldsAccessible(t *testing.T) { + // Confirm fields other than jobAgentVariables (release, resource etc.) + // are reachable when present on the DispatchContext. + dc := &oapi.DispatchContext{ + Resource: &oapi.Resource{Id: "res-1", Name: "srv-a"}, + } + cfg := oapi.JobAgentConfig{ + "appName": "argo-{{ .resource.name }}", + } + + out, err := renderJobAgentConfig(cfg, dc) + require.NoError(t, err) + assert.Equal(t, "argo-srv-a", out["appName"]) +} diff --git a/apps/workspace-engine/pkg/workspace/jobs/factory.go b/apps/workspace-engine/pkg/workspace/jobs/factory.go index 35a780906..95f64ae0f 100644 --- a/apps/workspace-engine/pkg/workspace/jobs/factory.go +++ b/apps/workspace-engine/pkg/workspace/jobs/factory.go @@ -4,13 +4,16 @@ package jobs import ( "context" "fmt" + "log/slog" "time" "github.com/google/uuid" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" oteltrace "go.opentelemetry.io/otel/trace" + "workspace-engine/pkg/db" "workspace-engine/pkg/oapi" + "workspace-engine/svc/controllers/desiredrelease/variableresolver" ) var tracer = otel.Tracer("workspace/releasemanager/jobs") @@ -21,15 +24,27 @@ type Getters interface { GetResource(ctx context.Context, resourceID uuid.UUID) (*oapi.Resource, error) } -// Factory creates jobs for releases. +// Factory creates jobs for releases. When secretResolver is non-nil and +// the dispatching job agent has job_agent-scoped variables, those are +// resolved at BuildDispatchContext time and surfaced under +// DispatchContext.JobAgentVariables for template interpolation. type Factory struct { - getters Getters + getters Getters + secretResolver variableresolver.SecretResolver } func NewFactoryFromGetters(getters Getters) *Factory { - return &Factory{ - getters: getters, - } + return &Factory{getters: getters} +} + +// NewFactoryWithSecrets constructs a Factory that resolves job-agent scoped +// secret_ref variables for every dispatch. Callers without a secret +// resolver should use NewFactoryFromGetters. +func NewFactoryWithSecrets( + getters Getters, + secretResolver variableresolver.SecretResolver, +) *Factory { + return &Factory{getters: getters, secretResolver: secretResolver} } // BuildDispatchContext builds a dispatch context for a release, fetching @@ -70,11 +85,68 @@ func (f *Factory) BuildDispatchContext( if jobAgent != nil { dc.JobAgent = *jobAgent dc.JobAgentConfig = jobAgent.Config + if err := f.populateJobAgentVariables(ctx, dc, jobAgent); err != nil { + return nil, err + } + // Template-render any `{{ ... }}` strings in the agent config + // against the dispatch context, so agent configs can reference + // resolved jobAgentVariables (e.g. apiKey: "{{ .jobAgentVariables.argo_token }}"). + rendered, err := renderJobAgentConfig(dc.JobAgentConfig, dc) + if err != nil { + return nil, fmt.Errorf("render job agent config: %w", err) + } + dc.JobAgentConfig = rendered } return dc, nil } +// populateJobAgentVariables resolves variables scoped to the dispatching +// job agent and writes them onto the DispatchContext. A nil secretResolver +// (NewFactoryFromGetters caller) short-circuits to a no-op; only callers +// that wired NewFactoryWithSecrets pay for the lookup. +func (f *Factory) populateJobAgentVariables( + ctx context.Context, + dc *oapi.DispatchContext, + jobAgent *oapi.JobAgent, +) error { + if f.secretResolver == nil { + return nil + } + jobAgentID, err := uuid.Parse(jobAgent.Id) + if err != nil { + return fmt.Errorf("parse job agent id: %w", err) + } + workspaceID, err := uuid.Parse(jobAgent.WorkspaceId) + if err != nil { + return fmt.Errorf("parse job agent workspace id: %w", err) + } + + getter := variableresolver.NewPostgresGetter(db.GetQueries(ctx)) + resolved, sensitiveKeys, err := variableresolver.ResolveForJobAgent( + ctx, + getter, + f.secretResolver, + workspaceID, + jobAgentID, + ) + if err != nil { + return fmt.Errorf("resolve job agent variables for %s: %w", jobAgentID, err) + } + if len(resolved) == 0 { + return nil + } + dc.JobAgentVariables = &resolved + if len(sensitiveKeys) > 0 { + slog.InfoContext(ctx, "job agent variables resolved", + "job_agent_id", jobAgentID.String(), + "resolved_count", len(resolved), + "sensitive_count", len(sensitiveKeys), + ) + } + return nil +} + // CreateJobForRelease creates a job for a given release (PURE FUNCTION, NO WRITES). // The job uses the resolved settings already present on the selected job agent. func (f *Factory) CreateJobForRelease( diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go b/apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go index ed28629e7..e94791110 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go @@ -88,6 +88,13 @@ func (m *mockReconcileGetter) GetResourceVariables( return m.resourceVar, nil } +func (m *mockReconcileGetter) GetJobAgentVariables( + _ context.Context, + _ uuid.UUID, +) ([]oapi.DeploymentVariableWithValues, error) { + return nil, nil +} + func (m *mockReconcileGetter) GetRelationshipRules( _ context.Context, _ uuid.UUID, diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/getters.go b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/getters.go index 0474ddb67..db0d1920d 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/getters.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/getters.go @@ -20,6 +20,10 @@ type Getter interface { ctx context.Context, resourceID string, ) (map[string][]oapi.ResourceVariable, error) + GetJobAgentVariables( + ctx context.Context, + jobAgentID uuid.UUID, + ) ([]oapi.DeploymentVariableWithValues, error) GetVariableSetsWithVariables( ctx context.Context, workspaceID uuid.UUID, diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/getters_postgres.go b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/getters_postgres.go index 534489eb3..0b90c4eb4 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/getters_postgres.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/getters_postgres.go @@ -124,6 +124,41 @@ func (g *PostgresGetter) GetDeploymentVariables( return result, nil } +func (g *PostgresGetter) GetJobAgentVariables( + ctx context.Context, + jobAgentID uuid.UUID, +) ([]oapi.DeploymentVariableWithValues, error) { + q := db.GetQueries(ctx) + + rows, err := q.ListVariablesWithValuesByJobAgentID(ctx, jobAgentID) + if err != nil { + return nil, fmt.Errorf("list job_agent variables for %s: %w", jobAgentID, err) + } + + result := make([]oapi.DeploymentVariableWithValues, 0, len(rows)) + for _, row := range rows { + var aggs []db.VariableValueAggRow + if err := json.Unmarshal(row.Values, &aggs); err != nil { + return nil, fmt.Errorf("unmarshal values for variable %s: %w", row.ID, err) + } + + oapiValues := make([]oapi.DeploymentVariableValue, 0, len(aggs)) + for _, a := range aggs { + val, err := db.ToOapiDeploymentVariableValueFromAgg(a) + if err != nil { + return nil, fmt.Errorf("map value %s: %w", a.ID, err) + } + oapiValues = append(oapiValues, val) + } + + result = append(result, oapi.DeploymentVariableWithValues{ + Variable: db.ToOapiJobAgentVariable(row), + Values: oapiValues, + }) + } + return result, nil +} + func (g *PostgresGetter) GetResourceVariables( ctx context.Context, resourceID string, diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/job_agent.go b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/job_agent.go new file mode 100644 index 000000000..f8a70967b --- /dev/null +++ b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/job_agent.go @@ -0,0 +1,114 @@ +package variableresolver + +import ( + "context" + "errors" + "fmt" + "sort" + + "github.com/google/uuid" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "workspace-engine/pkg/oapi" +) + +// JobAgentVarsGetter is the subset of Getter required to resolve +// job_agent-scoped variables. It exists so the jobs.Factory (which only +// needs this one method) can depend on the smaller interface. +type JobAgentVarsGetter interface { + GetJobAgentVariables( + ctx context.Context, + jobAgentID uuid.UUID, + ) ([]oapi.DeploymentVariableWithValues, error) +} + +// ResolveForJobAgent resolves every variable scoped to the given job agent +// and returns the resolved map plus the list of keys whose value originated +// from a secret_ref. Job-agent variables do not honor resource selectors — +// they apply to every dispatch through this agent — so only the highest +// priority candidate per key is evaluated. +// +// The boolean return on each helper is propagated through to populate +// release.EncryptedVariables. Errors wrapping ErrSecretResolution short +// circuit and block the dispatch. +func ResolveForJobAgent( + ctx context.Context, + getter JobAgentVarsGetter, + secretResolver SecretResolver, + workspaceID uuid.UUID, + jobAgentID uuid.UUID, +) (map[string]oapi.LiteralValue, []string, error) { + ctx, span := tracer.Start(ctx, "variableresolver.ResolveForJobAgent") + defer span.End() + span.SetAttributes( + attribute.String("workspace.id", workspaceID.String()), + attribute.String("job_agent.id", jobAgentID.String()), + ) + + vars, err := getter.GetJobAgentVariables(ctx, jobAgentID) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, "get job_agent variables failed") + return nil, nil, fmt.Errorf("get job_agent variables: %w", err) + } + span.SetAttributes(attribute.Int("job_agent_variables.count", len(vars))) + + if len(vars) == 0 { + return map[string]oapi.LiteralValue{}, nil, nil + } + + resolved := make(map[string]oapi.LiteralValue, len(vars)) + var sensitiveKeys []string + + for _, v := range vars { + key := v.Variable.Key + if len(v.Values) == 0 { + continue + } + + values := append([]oapi.DeploymentVariableValue(nil), v.Values...) + sort.Slice(values, func(i, j int) bool { + return values[i].Priority > values[j].Priority + }) + + for _, vv := range values { + // Job-agent variables resolve outside a release-target context, + // so reference values (which traverse related entities) cannot + // be resolved here. Skip non-literal, non-secret_ref kinds. + valueType, err := vv.Value.GetType() + if err != nil { + continue + } + if valueType != "literal" && valueType != "secret_ref" { + continue + } + lv, sensitive, err := ResolveValue( + ctx, + nil, + secretResolver, + workspaceID, + "", + nil, + &vv.Value, + ) + if errors.Is(err, ErrSecretResolution) { + span.RecordError(err) + span.SetStatus(codes.Error, "resolve job_agent secret_ref failed") + return nil, nil, fmt.Errorf("resolve job_agent variable %q: %w", key, err) + } + if err == nil && lv != nil { + resolved[key] = *lv + if sensitive { + sensitiveKeys = append(sensitiveKeys, key) + } + break + } + } + } + + span.SetAttributes( + attribute.Int("job_agent_variables.resolved", len(resolved)), + attribute.Int("job_agent_variables.sensitive", len(sensitiveKeys)), + ) + return resolved, sensitiveKeys, nil +} diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go index 59031c068..dc71a1e79 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go @@ -31,9 +31,11 @@ func (m *mockResolver) ResolveRelated( // mock Getter (for Resolve tests) // --------------------------------------------------------------------------- +// mockGetter is the in-memory variableresolver.Getter used by these tests. type mockGetter struct { deploymentVars []oapi.DeploymentVariableWithValues resourceVars map[string][]oapi.ResourceVariable + jobAgentVars []oapi.DeploymentVariableWithValues variableSets []oapi.VariableSetWithVariables rules []eval.Rule candidates map[string][]eval.EntityData @@ -77,6 +79,13 @@ func (m *mockGetter) GetResourceVariables( return m.resourceVars, nil } +func (m *mockGetter) GetJobAgentVariables( + _ context.Context, + _ uuid.UUID, +) ([]oapi.DeploymentVariableWithValues, error) { + return m.jobAgentVars, nil +} + func (m *mockGetter) GetVariableSetsWithVariables( ctx context.Context, workspaceID uuid.UUID, diff --git a/apps/workspace-engine/svc/controllers/forcedeploy/controller.go b/apps/workspace-engine/svc/controllers/forcedeploy/controller.go index 295d2af68..820e61181 100644 --- a/apps/workspace-engine/svc/controllers/forcedeploy/controller.go +++ b/apps/workspace-engine/svc/controllers/forcedeploy/controller.go @@ -16,14 +16,16 @@ import ( "workspace-engine/pkg/reconcile/events" "workspace-engine/pkg/reconcile/postgres" "workspace-engine/svc" + "workspace-engine/svc/controllers/desiredrelease/variableresolver" ) var tracer = otel.Tracer("workspace-engine/svc/controllers/forcedeploy") var _ reconcile.Processor = (*Controller)(nil) type Controller struct { - getter Getter - setter Setter + getter Getter + setter Setter + secretResolver variableresolver.SecretResolver } func (c *Controller) Process(ctx context.Context, item reconcile.Item) (reconcile.Result, error) { @@ -50,7 +52,7 @@ func (c *Controller) Process(ctx context.Context, item reconcile.Item) (reconcil return reconcile.Result{}, nil } - result, err := Reconcile(ctx, item.WorkspaceID, c.getter, c.setter, rt) + result, err := Reconcile(ctx, item.WorkspaceID, c.getter, c.setter, c.secretResolver, rt) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) @@ -66,11 +68,19 @@ func (c *Controller) Process(ctx context.Context, item reconcile.Item) (reconcil // NewController creates a Controller with the given dependencies. // Use this constructor in tests to inject mock implementations. -func NewController(getter Getter, setter Setter) *Controller { - return &Controller{getter: getter, setter: setter} +func NewController( + getter Getter, + setter Setter, + secretResolver variableresolver.SecretResolver, +) *Controller { + return &Controller{getter: getter, setter: setter, secretResolver: secretResolver} } -func New(workerID string, pgxPool *pgxpool.Pool) svc.Service { +func New( + workerID string, + pgxPool *pgxpool.Pool, + secretResolver variableresolver.SecretResolver, +) svc.Service { if pgxPool == nil { slog.Error("Failed to get pgx pool") os.Exit(1) @@ -91,8 +101,9 @@ func New(workerID string, pgxPool *pgxpool.Pool) svc.Service { queue := postgres.NewForKinds(pgxPool, kind) controller := &Controller{ - getter: &PostgresGetter{}, - setter: &PostgresSetter{}, + getter: &PostgresGetter{}, + setter: &PostgresSetter{}, + secretResolver: secretResolver, } worker, err := reconcile.NewWorker(kind, queue, controller, nodeConfig) diff --git a/apps/workspace-engine/svc/controllers/forcedeploy/reconcile.go b/apps/workspace-engine/svc/controllers/forcedeploy/reconcile.go index 0e7a65ad9..114a9b3f7 100644 --- a/apps/workspace-engine/svc/controllers/forcedeploy/reconcile.go +++ b/apps/workspace-engine/svc/controllers/forcedeploy/reconcile.go @@ -13,6 +13,7 @@ import ( "workspace-engine/pkg/oapi" "workspace-engine/pkg/selector" "workspace-engine/pkg/workspace/jobs" + "workspace-engine/svc/controllers/desiredrelease/variableresolver" ) const requeueDelay = 5 * time.Second @@ -26,6 +27,7 @@ func Reconcile( workspaceID string, getter Getter, setter Setter, + secretResolver variableresolver.SecretResolver, rt *ReleaseTarget, ) (*ReconcileResult, error) { ctx, span := tracer.Start(ctx, "forcedeploy.Reconcile") @@ -69,6 +71,7 @@ func Reconcile( workspaceUUID, getter, setter, + secretResolver, rt, release, ); err != nil { @@ -84,6 +87,7 @@ func buildAndDispatchJob( workspaceID uuid.UUID, getter Getter, setter Setter, + secretResolver variableresolver.SecretResolver, rt *ReleaseTarget, release *oapi.Release, ) error { @@ -131,7 +135,7 @@ func buildAndDispatchJob( ) } - factory := jobs.NewFactoryFromGetters(getter) + factory := jobs.NewFactoryWithSecrets(getter, secretResolver) for i := range matchedAgents { agent := &matchedAgents[i] agent.Config = oapi.DeepMergeConfigs( diff --git a/apps/workspace-engine/svc/controllers/forcedeploy/reconcile_test.go b/apps/workspace-engine/svc/controllers/forcedeploy/reconcile_test.go index 815566b93..a253cf827 100644 --- a/apps/workspace-engine/svc/controllers/forcedeploy/reconcile_test.go +++ b/apps/workspace-engine/svc/controllers/forcedeploy/reconcile_test.go @@ -193,7 +193,7 @@ func TestReconcile_HappyPath_CreatesJobAndEnqueuesDispatch(t *testing.T) { release := testRelease(rt) getter, setter := defaultMocks(rt, release) - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) assert.Zero(t, result.RequeueAfter) @@ -215,7 +215,7 @@ func TestReconcile_HappyPath_WithCompletedJob(t *testing.T) { // are not included. getter.activeJobs = []*oapi.Job{} - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) assert.Zero(t, result.RequeueAfter) @@ -230,7 +230,7 @@ func TestReconcile_NoDesiredRelease_Noop(t *testing.T) { } setter := &mockSetter{} - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) assert.Zero(t, result.RequeueAfter) @@ -254,7 +254,7 @@ func TestReconcile_ActiveJobExists_Requeues(t *testing.T) { }, } - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) assert.Equal(t, requeueDelay, result.RequeueAfter) @@ -278,7 +278,7 @@ func TestReconcile_ActivePendingJob_Requeues(t *testing.T) { }, } - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) assert.Equal(t, requeueDelay, result.RequeueAfter) @@ -301,7 +301,7 @@ func TestProcess_ActiveJob_ReturnsRequeueResult(t *testing.T) { }, } - ctrl := NewController(getter, setter) + ctrl := NewController(getter, setter, nil) item := reconcile.Item{ ID: 1, WorkspaceID: rt.WorkspaceID.String(), diff --git a/apps/workspace-engine/svc/controllers/jobeligibility/controller.go b/apps/workspace-engine/svc/controllers/jobeligibility/controller.go index 18fb48567..9a99f127b 100644 --- a/apps/workspace-engine/svc/controllers/jobeligibility/controller.go +++ b/apps/workspace-engine/svc/controllers/jobeligibility/controller.go @@ -17,14 +17,16 @@ import ( "workspace-engine/pkg/reconcile/postgres" "workspace-engine/pkg/store/policies" "workspace-engine/svc" + "workspace-engine/svc/controllers/desiredrelease/variableresolver" ) var tracer = otel.Tracer("workspace-engine/svc/controllers/jobeligibility") var _ reconcile.Processor = (*Controller)(nil) type Controller struct { - getter Getter - setter Setter + getter Getter + setter Setter + secretResolver variableresolver.SecretResolver } // Process implements [reconcile.Processor]. @@ -56,7 +58,7 @@ func (c *Controller) Process(ctx context.Context, item reconcile.Item) (reconcil return reconcile.Result{}, nil } - result, err := Reconcile(ctx, item.WorkspaceID, c.getter, c.setter, rt) + result, err := Reconcile(ctx, item.WorkspaceID, c.getter, c.setter, c.secretResolver, rt) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) @@ -75,11 +77,19 @@ func (c *Controller) Process(ctx context.Context, item reconcile.Item) (reconcil // NewController creates a Controller with the given dependencies. // Use this constructor in tests to inject mock implementations. -func NewController(getter Getter, setter Setter) *Controller { - return &Controller{getter: getter, setter: setter} +func NewController( + getter Getter, + setter Setter, + secretResolver variableresolver.SecretResolver, +) *Controller { + return &Controller{getter: getter, setter: setter, secretResolver: secretResolver} } -func New(workerID string, pgxPool *pgxpool.Pool) svc.Service { +func New( + workerID string, + pgxPool *pgxpool.Pool, + secretResolver variableresolver.SecretResolver, +) svc.Service { if pgxPool == nil { slog.Error("Failed to get pgx pool") os.Exit(1) @@ -108,7 +118,8 @@ func New(workerID string, pgxPool *pgxpool.Pool) svc.Service { getter: NewPostgresGetter( policies.NewPostgresGetPoliciesForReleaseTarget(policies.WithCache(5 * time.Minute)), ), - setter: &PostgresSetter{Queue: enqueueQueue}, + setter: &PostgresSetter{Queue: enqueueQueue}, + secretResolver: secretResolver, } worker, err := reconcile.NewWorker( kind, diff --git a/apps/workspace-engine/svc/controllers/jobeligibility/reconcile.go b/apps/workspace-engine/svc/controllers/jobeligibility/reconcile.go index 1b5243e8c..d468d11fb 100644 --- a/apps/workspace-engine/svc/controllers/jobeligibility/reconcile.go +++ b/apps/workspace-engine/svc/controllers/jobeligibility/reconcile.go @@ -15,6 +15,7 @@ import ( "workspace-engine/pkg/workspace/releasemanager/policy/evaluator" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/releasetargetconcurrency" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/retry" + "workspace-engine/svc/controllers/desiredrelease/variableresolver" ) type ReconcileResult struct { @@ -24,9 +25,10 @@ type ReconcileResult struct { type reconciler struct { workspaceID uuid.UUID - getter Getter - setter Setter - rt *ReleaseTarget + getter Getter + setter Setter + secretResolver variableresolver.SecretResolver + rt *ReleaseTarget release *oapi.Release policies []*oapi.Policy @@ -151,7 +153,7 @@ func (r *reconciler) createFailureJob( ) error { now := time.Now() - factory := jobs.NewFactoryFromGetters(r.getter) + factory := jobs.NewFactoryWithSecrets(r.getter, r.secretResolver) deploymentID, err := uuid.Parse(r.release.ReleaseTarget.DeploymentId) if err != nil { return fmt.Errorf("parse deployment id: %w", err) @@ -278,7 +280,7 @@ func (r *reconciler) buildAndDispatchJob(ctx context.Context) error { agent.Config, deployment.JobAgentConfig, r.release.Version.JobAgentConfig, ) - job, err := jobs.NewFactoryFromGetters(r.getter). + job, err := jobs.NewFactoryWithSecrets(r.getter, r.secretResolver). CreateJobForRelease(ctx, r.release, agent) if err != nil { return recordErr(span, "build job", err) @@ -303,6 +305,7 @@ func Reconcile( workspaceID string, getter Getter, setter Setter, + secretResolver variableresolver.SecretResolver, rt *ReleaseTarget, ) (*ReconcileResult, error) { ctx, span := tracer.Start(ctx, "jobeligibility.Reconcile") @@ -313,7 +316,13 @@ func Reconcile( return nil, fmt.Errorf("parse workspace id: %w", err) } - r := &reconciler{workspaceID: workspaceIDUUID, getter: getter, setter: setter, rt: rt} + r := &reconciler{ + workspaceID: workspaceIDUUID, + getter: getter, + setter: setter, + secretResolver: secretResolver, + rt: rt, + } r.rt.WorkspaceID = r.workspaceID if err := r.loadInput(ctx); err != nil { diff --git a/apps/workspace-engine/svc/controllers/jobeligibility/reconcile_test.go b/apps/workspace-engine/svc/controllers/jobeligibility/reconcile_test.go index 8efb60838..8a3adc006 100644 --- a/apps/workspace-engine/svc/controllers/jobeligibility/reconcile_test.go +++ b/apps/workspace-engine/svc/controllers/jobeligibility/reconcile_test.go @@ -338,7 +338,7 @@ func TestNewReleaseTarget_NonUUIDLast(t *testing.T) { func TestProcess_InvalidScopeID(t *testing.T) { getter := &mockGetter{} setter := &mockSetter{} - ctrl := NewController(getter, setter) + ctrl := NewController(getter, setter, nil) item := reconcile.Item{ ID: 1, @@ -356,7 +356,7 @@ func TestProcess_ReleaseTargetNotFound(t *testing.T) { rt := testRT() getter := &mockGetter{rtExists: false} setter := &mockSetter{} - ctrl := NewController(getter, setter) + ctrl := NewController(getter, setter, nil) item := reconcile.Item{ ID: 1, @@ -374,7 +374,7 @@ func TestProcess_ReleaseTargetExistsCheckFails(t *testing.T) { rt := testRT() getter := &mockGetter{rtExistsErr: fmt.Errorf("db error")} setter := &mockSetter{} - ctrl := NewController(getter, setter) + ctrl := NewController(getter, setter, nil) item := reconcile.Item{ ID: 1, @@ -394,7 +394,7 @@ func TestProcess_ReconcileError(t *testing.T) { releaseErr: fmt.Errorf("release fetch failed"), } setter := &mockSetter{} - ctrl := NewController(getter, setter) + ctrl := NewController(getter, setter, nil) item := reconcile.Item{ ID: 1, @@ -433,7 +433,7 @@ func TestProcess_RequeueOnBackoff(t *testing.T) { workspaceAgents: []oapi.JobAgent{*agent}, } setter := &mockSetter{} - ctrl := NewController(getter, setter) + ctrl := NewController(getter, setter, nil) item := reconcile.Item{ ID: 1, @@ -456,7 +456,7 @@ func TestReconcile_NoDesiredRelease(t *testing.T) { getter := &mockGetter{release: nil} setter := &mockSetter{} - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) assert.NotNil(t, result) assert.Nil(t, result.NextReconcileAt) @@ -468,7 +468,7 @@ func TestReconcile_GetDesiredReleaseFails(t *testing.T) { getter := &mockGetter{releaseErr: fmt.Errorf("db down")} setter := &mockSetter{} - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.Error(t, err) assert.Contains(t, err.Error(), "get desired release") } @@ -483,7 +483,7 @@ func TestReconcile_GetPoliciesFails(t *testing.T) { } setter := &mockSetter{} - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.Error(t, err) assert.Contains(t, err.Error(), "get policies for release target") } @@ -493,7 +493,7 @@ func TestReconcile_InvalidWorkspaceID(t *testing.T) { getter := &mockGetter{} setter := &mockSetter{} - _, err := Reconcile(context.Background(), "not-a-uuid", getter, setter, rt) + _, err := Reconcile(context.Background(), "not-a-uuid", getter, setter, nil, rt) require.Error(t, err) assert.Contains(t, err.Error(), "parse workspace id") } @@ -503,7 +503,7 @@ func TestReconcile_HappyPath_CreatesAndDispatchesJob(t *testing.T) { release := testRelease(rt) getter, setter := setupHappyPath(rt, release) - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) assert.NotNil(t, result) assert.Nil(t, result.NextReconcileAt) @@ -540,6 +540,7 @@ func TestReconcile_ActiveJobBlocks(t *testing.T) { rt.WorkspaceID.String(), getter, setter, + nil, rt, ) require.NoError(t, err) @@ -582,6 +583,7 @@ func TestReconcile_TerminalStatusDoesNotBlock(t *testing.T) { rt.WorkspaceID.String(), getter, setter, + nil, rt, ) require.NoError(t, err) @@ -607,7 +609,7 @@ func TestReconcile_ActiveJobFromDifferentReleaseStillBlocks(t *testing.T) { getter.jobs = []*oapi.Job{activeJob} getter.processingJobs = []*oapi.Job{activeJob} - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) assert.Empty(t, setter.createdJobs, "an active job from a different release should still block") assert.Nil(t, result.NextReconcileAt) @@ -624,7 +626,7 @@ func TestReconcile_NoPolicyNoJobs_FirstAttemptAllowed(t *testing.T) { getter.jobs = []*oapi.Job{} getter.policies = []*oapi.Policy{} - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) assert.NotNil(t, result) require.Len(t, setter.createdJobs, 1, "first attempt should be allowed") @@ -656,6 +658,7 @@ func TestReconcile_NoPolicyOneCompletedJob_Denied(t *testing.T) { rt.WorkspaceID.String(), getter, setter, + nil, rt, ) require.NoError(t, err) @@ -681,7 +684,7 @@ func TestReconcile_PolicyMaxRetries3_NoFailures_Allowed(t *testing.T) { getter.policies = []*oapi.Policy{testPolicy(true, &oapi.RetryRule{MaxRetries: 3})} getter.jobs = []*oapi.Job{} - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.createdJobs, 1) assert.Nil(t, result.NextReconcileAt) @@ -703,7 +706,7 @@ func TestReconcile_PolicyMaxRetries3_AtLimit_Allowed(t *testing.T) { } getter.jobs = jobs - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len( t, @@ -730,7 +733,7 @@ func TestReconcile_PolicyMaxRetries3_Exceeded_Denied(t *testing.T) { } getter.jobs = jobs - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) assert.Empty(t, setter.createdJobs, "exceeding maxRetries should deny job creation") assert.Nil(t, result.NextReconcileAt) @@ -745,7 +748,7 @@ func TestReconcile_PolicyMaxRetries0_OneAttemptOnly(t *testing.T) { failedJob := testJobForRelease(release, oapi.JobStatusFailure, time.Now().Add(-time.Minute)) getter.jobs = []*oapi.Job{failedJob} - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) assert.Empty(t, setter.createdJobs, "maxRetries=0 should only allow one attempt") assert.Nil(t, result.NextReconcileAt) @@ -777,7 +780,7 @@ func TestReconcile_ExplicitRetryOnStatuses_OnlyCountsThose(t *testing.T) { failedJob := testJobForRelease(release, oapi.JobStatusFailure, time.Now().Add(-time.Minute)) getter.jobs = []*oapi.Job{cancelledJob, failedJob} - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len( t, @@ -805,7 +808,7 @@ func TestReconcile_DefaultRetryOnStatuses_MaxRetriesGT0(t *testing.T) { ) getter.jobs = []*oapi.Job{successfulJob, failedJob} - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len( t, @@ -829,7 +832,7 @@ func TestReconcile_DefaultRetryOnStatuses_MaxRetries0_SuccessfulCounts(t *testin ) getter.jobs = []*oapi.Job{successfulJob} - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) assert.Empty( t, @@ -855,7 +858,7 @@ func TestReconcile_DifferentReleaseBreaksRetryChain(t *testing.T) { oldJob := testJobForRelease(oldRelease, oapi.JobStatusFailure, time.Now().Add(-30*time.Second)) getter.jobs = []*oapi.Job{oldJob} - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len( t, @@ -886,7 +889,7 @@ func TestReconcile_LinearBackoff_WithinWindow_Denied(t *testing.T) { completedAt.Add(-time.Second), completedAt) getter.jobs = []*oapi.Job{failedJob} - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) assert.Empty(t, setter.createdJobs, "should not create job during backoff window") require.NotNil(t, result.NextReconcileAt, "should schedule requeue") @@ -913,7 +916,7 @@ func TestReconcile_LinearBackoff_PastWindow_Allowed(t *testing.T) { completedAt.Add(-time.Second), completedAt) getter.jobs = []*oapi.Job{failedJob} - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.createdJobs, 1, "should create job after backoff window") assert.Nil(t, result.NextReconcileAt) @@ -952,7 +955,7 @@ func TestReconcile_ExponentialBackoff_SecondAttempt(t *testing.T) { ) getter.jobs = []*oapi.Job{job2, job1} // newest first - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) assert.Empty(t, setter.createdJobs, "should be in exponential backoff (20s, only 4s elapsed)") require.NotNil(t, result.NextReconcileAt) @@ -986,7 +989,7 @@ func TestReconcile_ExponentialBackoff_WithMaxCap(t *testing.T) { jobs[0].CompletedAt = &recentCompleted getter.jobs = jobs - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) assert.Empty( t, @@ -1013,7 +1016,7 @@ func TestReconcile_DisabledPolicyIgnored(t *testing.T) { failedJob := testJobForRelease(release, oapi.JobStatusFailure, time.Now().Add(-time.Minute)) getter.jobs = []*oapi.Job{failedJob} - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) assert.Empty( t, @@ -1038,7 +1041,7 @@ func TestReconcile_FirstEnabledPolicyRetryRuleWins(t *testing.T) { failedJob := testJobForRelease(release, oapi.JobStatusFailure, time.Now().Add(-time.Minute)) getter.jobs = []*oapi.Job{failedJob} - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) assert.Empty( t, @@ -1063,7 +1066,7 @@ func TestReconcile_DisabledPolicySkipped_EnabledUsed(t *testing.T) { failedJob := testJobForRelease(release, oapi.JobStatusFailure, time.Now().Add(-time.Minute)) getter.jobs = []*oapi.Job{failedJob} - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len( t, @@ -1088,7 +1091,7 @@ func TestReconcile_PolicyWithNoRetryRule_SkippedToNextPolicy(t *testing.T) { failedJob := testJobForRelease(release, oapi.JobStatusFailure, time.Now().Add(-time.Minute)) getter.jobs = []*oapi.Job{failedJob} - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.createdJobs, 1, "should use second policy's retry rule") assert.Nil(t, result.NextReconcileAt) @@ -1103,7 +1106,7 @@ func TestReconcile_CreatedJobHasPendingStatus(t *testing.T) { release := testRelease(rt) getter, setter := setupHappyPath(rt, release) - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.createdJobs, 1) assert.Equal(t, oapi.JobStatusPending, setter.createdJobs[0].Status) @@ -1114,7 +1117,7 @@ func TestReconcile_CreatedJobHasCorrectReleaseID(t *testing.T) { release := testRelease(rt) getter, setter := setupHappyPath(rt, release) - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.createdJobs, 1) assert.Equal(t, release.Id.String(), setter.createdJobs[0].ReleaseId) @@ -1125,7 +1128,7 @@ func TestReconcile_CreatedJobHasDispatchContext(t *testing.T) { release := testRelease(rt) getter, setter := setupHappyPath(rt, release) - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.createdJobs, 1) @@ -1146,7 +1149,7 @@ func TestReconcile_CreatedJobHasValidUUID(t *testing.T) { release := testRelease(rt) getter, setter := setupHappyPath(rt, release) - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.createdJobs, 1) @@ -1160,7 +1163,7 @@ func TestReconcile_NoJobAgentSelector_CreatesFailureJob(t *testing.T) { getter, setter := setupHappyPath(rt, release) getter.deployment.JobAgentSelector = "" - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.createdJobs, 1) assert.Equal(t, oapi.JobStatusInvalidJobAgent, setter.createdJobs[0].Status) @@ -1173,7 +1176,7 @@ func TestReconcile_NilJobAgentSelector_CreatesFailureJob(t *testing.T) { getter, setter := setupHappyPath(rt, release) getter.deployment.JobAgentSelector = "" - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.createdJobs, 1) assert.Equal(t, oapi.JobStatusInvalidJobAgent, setter.createdJobs[0].Status) @@ -1186,7 +1189,7 @@ func TestReconcile_NoMatchingAgents_CreatesFailureJob(t *testing.T) { getter, setter := setupHappyPath(rt, release) getter.workspaceAgents = []oapi.JobAgent{} - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.createdJobs, 1) assert.Equal(t, oapi.JobStatusInvalidJobAgent, setter.createdJobs[0].Status) @@ -1234,7 +1237,7 @@ func TestReconcile_NoMatchingAgents_IncludesMissingKeyDiagnostic(t *testing.T) { } setter := &mockSetter{} - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.createdJobs, 1) assert.Equal(t, oapi.JobStatusInvalidJobAgent, setter.createdJobs[0].Status) @@ -1274,7 +1277,7 @@ func TestReconcile_MultipleJobAgents_CreatesMultipleJobs(t *testing.T) { } setter := &mockSetter{} - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) assert.Nil(t, result.NextReconcileAt) require.Len(t, setter.createdJobs, 2, "should create one job per agent") @@ -1301,7 +1304,7 @@ func TestReconcile_GetDeploymentFails_Error(t *testing.T) { getter.deploymentErr = fmt.Errorf("deployment not found") getter.deployment = nil - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.Error(t, err) assert.Contains(t, err.Error(), "get deployment") } @@ -1313,7 +1316,7 @@ func TestReconcile_GetEnvironmentFails_Error(t *testing.T) { getter.environmentErr = fmt.Errorf("env not found") getter.environment = nil - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.Error(t, err) assert.Contains(t, err.Error(), "get environment") } @@ -1325,7 +1328,7 @@ func TestReconcile_GetResourceFails_Error(t *testing.T) { getter.resourceErr = fmt.Errorf("resource not found") getter.resource = nil - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.Error(t, err) assert.Contains(t, err.Error(), "get resource") } @@ -1336,7 +1339,7 @@ func TestReconcile_CreateJobFails_Error(t *testing.T) { getter, setter := setupHappyPath(rt, release) setter.createJobErr = fmt.Errorf("create failed") - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.Error(t, err) assert.Contains(t, err.Error(), "create job") assert.Empty(t, setter.enqueueCalls, "should not enqueue when job creation fails") @@ -1348,7 +1351,7 @@ func TestReconcile_EnqueueFails_Error(t *testing.T) { getter, setter := setupHappyPath(rt, release) setter.enqueueErr = fmt.Errorf("enqueue failed") - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.Error(t, err) assert.Contains(t, err.Error(), "enqueue job dispatch") } @@ -1368,7 +1371,7 @@ func TestReconcile_JobsSortedByCreatedAt(t *testing.T) { newer := testJobForRelease(release, oapi.JobStatusFailure, time.Now().Add(-1*time.Minute)) getter.jobs = []*oapi.Job{old, newer} // intentionally out of order - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len( t, @@ -1396,7 +1399,7 @@ func TestReconcile_BackoffUsesCompletedAtWhenAvailable(t *testing.T) { failedJob := testJobWithCompletion(release, oapi.JobStatusFailure, createdAt, completedAt) getter.jobs = []*oapi.Job{failedJob} - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) assert.Empty(t, setter.createdJobs, "should use completedAt for backoff timing, not createdAt") require.NotNil(t, result.NextReconcileAt) @@ -1414,7 +1417,7 @@ func TestReconcile_NoBackoffWhenBackoffSecondsNil(t *testing.T) { failedJob := testJobForRelease(release, oapi.JobStatusFailure, time.Now().Add(-time.Second)) getter.jobs = []*oapi.Job{failedJob} - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.createdJobs, 1, "without backoff configured, retry should be immediate") assert.Nil(t, result.NextReconcileAt) @@ -1434,7 +1437,7 @@ func TestReconcile_NoBackoffWhenBackoffSecondsZero(t *testing.T) { failedJob := testJobForRelease(release, oapi.JobStatusFailure, time.Now().Add(-time.Second)) getter.jobs = []*oapi.Job{failedJob} - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.createdJobs, 1, "backoffSeconds=0 should not delay retry") assert.Nil(t, result.NextReconcileAt) @@ -1460,7 +1463,7 @@ func TestReconcile_MixedJobStatuses_ConsecutiveCounting(t *testing.T) { j4 := testJobForRelease(release, oapi.JobStatusFailure, now.Add(-40*time.Second)) getter.jobs = []*oapi.Job{j1, j2, j3, j4} - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len( t, @@ -1476,7 +1479,7 @@ func TestReconcile_EnqueueCalledWithCorrectWorkspaceID(t *testing.T) { release := testRelease(rt) getter, setter := setupHappyPath(rt, release) - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.enqueueCalls, 1) assert.Equal(t, rt.WorkspaceID.String(), setter.enqueueCalls[0].WorkspaceID) @@ -1489,7 +1492,7 @@ func TestReconcile_NoJobsNoPolices_FirstAttempt(t *testing.T) { getter.jobs = []*oapi.Job{} getter.policies = []*oapi.Policy{} - result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + result, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.createdJobs, 1) assert.Nil(t, result.NextReconcileAt) @@ -1557,7 +1560,7 @@ func TestReconcile_JobAgentConfig_DeepMergesThreeLevels(t *testing.T) { } setter := &mockSetter{} - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.createdJobs, 1) @@ -1636,7 +1639,7 @@ func TestReconcile_SelectorMatchesSpecificAgent(t *testing.T) { } setter := &mockSetter{} - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.createdJobs, 1, "should create exactly one job for the matched agent") assert.Equal(t, target.Id, setter.createdJobs[0].JobAgentId) @@ -1697,7 +1700,7 @@ func TestReconcile_SelectorByType_MatchesMultiple(t *testing.T) { } setter := &mockSetter{} - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.createdJobs, 2, "should create jobs for both argo-cd agents") @@ -1743,7 +1746,7 @@ func TestReconcile_SelectorFalse_NoAgentsMatched(t *testing.T) { } setter := &mockSetter{} - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.createdJobs, 1) assert.Equal(t, oapi.JobStatusInvalidJobAgent, setter.createdJobs[0].Status) @@ -1794,7 +1797,7 @@ func TestReconcile_SelectorByName_MatchesSingle(t *testing.T) { } setter := &mockSetter{} - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.createdJobs, 1) assert.Equal(t, prod.Id, setter.createdJobs[0].JobAgentId) @@ -1856,7 +1859,7 @@ func TestReconcile_ResourceAwareSelector_MatchesAgentByResourceConfig(t *testing } setter := &mockSetter{} - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.createdJobs, 1) assert.Equal(t, argoUS.Id, setter.createdJobs[0].JobAgentId) @@ -1906,7 +1909,7 @@ func TestReconcile_ResourceAwareSelector_NoMatch(t *testing.T) { } setter := &mockSetter{} - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.createdJobs, 1) assert.Equal(t, oapi.JobStatusInvalidJobAgent, setter.createdJobs[0].Status) @@ -1963,7 +1966,7 @@ func TestReconcile_ResourceAwareSelector_MatchesByMetadata(t *testing.T) { } setter := &mockSetter{} - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.createdJobs, 1) assert.Equal(t, agentProd.Id, setter.createdJobs[0].JobAgentId) @@ -2029,7 +2032,7 @@ func TestReconcile_ResourceAwareSelector_MultipleAgentsMatch(t *testing.T) { } setter := &mockSetter{} - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.createdJobs, 2) @@ -2097,7 +2100,7 @@ func TestReconcile_ResourceAwareSelector_MixedWithJobAgentFields(t *testing.T) { } setter := &mockSetter{} - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.createdJobs, 1) assert.Equal(t, argoUS.Id, setter.createdJobs[0].JobAgentId) @@ -2143,7 +2146,7 @@ func TestReconcile_ResourceAwareSelector_MissingResourceConfigKey(t *testing.T) } setter := &mockSetter{} - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.NoError(t, err) require.Len(t, setter.createdJobs, 1) assert.Equal(t, oapi.JobStatusInvalidJobAgent, setter.createdJobs[0].Status) @@ -2174,7 +2177,7 @@ func TestReconcile_ResourceAwareSelector_GetResourceFails(t *testing.T) { } setter := &mockSetter{} - _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, rt) + _, err := Reconcile(context.Background(), rt.WorkspaceID.String(), getter, setter, nil, rt) require.Error(t, err) assert.Contains(t, err.Error(), "get resource") } diff --git a/apps/workspace-engine/test/controllers/harness/mocks.go b/apps/workspace-engine/test/controllers/harness/mocks.go index c7d00b5f1..b63d81bf0 100644 --- a/apps/workspace-engine/test/controllers/harness/mocks.go +++ b/apps/workspace-engine/test/controllers/harness/mocks.go @@ -255,6 +255,13 @@ func (g *DesiredReleaseGetter) GetResourceVariables( return g.ResourceVars, nil } +func (g *DesiredReleaseGetter) GetJobAgentVariables( + _ context.Context, + _ uuid.UUID, +) ([]oapi.DeploymentVariableWithValues, error) { + return nil, nil +} + func (g *DesiredReleaseGetter) GetRelationshipRules( _ context.Context, _ uuid.UUID, From bad627a0217a50d95b542774c560817ba9e16907 Mon Sep 17 00:00:00 2001 From: Michael Leone Date: Thu, 14 May 2026 09:12:26 -0400 Subject: [PATCH 10/13] feat(secrets): support secret_version pinning end-to-end Lets variable_value rows pin a specific provider-side version of a secret. Useful for reproducibility (a release dispatched against version N keeps resolving the same value regardless of rotation) and for tying a release to a known-good upstream secret. - packages/db: variable_value gains a nullable secret_version text column. Drizzle migration 0196_third_reavers.sql. - workspace-engine schema mirror (pkg/db/queries/schema.sql) tracks the column; queries/variables.sql json_agg blocks now emit secretVersion; sqlc regenerated. VariableValueAggRow gets SecretVersion *string. - oapi: SecretReferenceValue gains an optional secretVersion field with a description of AWS / Doppler / env semantics. Regenerated. - pkg/db/convert.go: the secret_ref kind path copies secret_version onto the SecretReferenceValue when non-empty (nil stays nil so resolvers can treat it as "latest"). - pkg/secrets/types.go: SecretReference grows a Version string. - pkg/secrets/cache.go: cacheKey gains Version, so latest vs pinned-v1 vs pinned-v2 are distinct cache entries. Pinned entries are effectively immutable for the TTL window (upstream cannot change a past version); latest entries still TTL-gate to pick up rotations. - variableresolver/value.go: resolveSecretReference passes srv.SecretVersion through to the secrets.SecretReference. - pkg/secrets/awssm: when Version is set, route to VersionId (UUID form) or VersionStage (anything starting with AWS, plus the "stage:" prefix for user-defined stages with the prefix stripped). Empty stays AWSCURRENT. - pkg/secrets/doppler: when Version is set, attach accept_secret_version= as a query parameter; otherwise omit it. - pkg/secrets/env: ignored (process env has no version). Tests cover the UUID / AWSCURRENT / stage-prefix / empty paths for awssm, the query-param presence/absence for doppler, and cache key disambiguation across multiple pinned versions plus latest. Existing tests untouched. --- apps/workspace-engine/oapi/openapi.json | 4 + .../oapi/spec/schemas/core.jsonnet | 4 + apps/workspace-engine/pkg/db/convert.go | 5 + apps/workspace-engine/pkg/db/models.go | 1 + .../pkg/db/queries/schema.sql | 1 + .../pkg/db/queries/variables.sql | 12 +- apps/workspace-engine/pkg/db/variables.sql.go | 12 +- apps/workspace-engine/pkg/oapi/oapi.gen.go | 3 + .../pkg/secrets/awssm/provider.go | 34 +- .../pkg/secrets/awssm/provider_test.go | 78 + apps/workspace-engine/pkg/secrets/cache.go | 2 + .../pkg/secrets/cache_test.go | 25 + .../pkg/secrets/doppler/provider.go | 3 + .../pkg/secrets/doppler/provider_test.go | 40 + apps/workspace-engine/pkg/secrets/types.go | 5 + .../desiredrelease/variableresolver/value.go | 3 + packages/db/drizzle/0196_third_reavers.sql | 1 + packages/db/drizzle/meta/0196_snapshot.json | 7613 +++++++++++++++++ packages/db/drizzle/meta/_journal.json | 7 + packages/db/src/schema/variable.ts | 1 + 20 files changed, 7844 insertions(+), 10 deletions(-) create mode 100644 packages/db/drizzle/0196_third_reavers.sql create mode 100644 packages/db/drizzle/meta/0196_snapshot.json diff --git a/apps/workspace-engine/oapi/openapi.json b/apps/workspace-engine/oapi/openapi.json index 6e82acc61..2d52cb34a 100644 --- a/apps/workspace-engine/oapi/openapi.json +++ b/apps/workspace-engine/oapi/openapi.json @@ -2530,6 +2530,10 @@ "secretProvider": { "description": "Workspace-unique secret_provider.name", "type": "string" + }, + "secretVersion": { + "description": "Optional provider-specific version pin. For AWS Secrets Manager this maps to VersionId (uuid form) or VersionStage (AWSCURRENT/AWSPREVIOUS). For Doppler this maps to accept_secret_version. Empty means latest.", + "type": "string" } }, "required": [ diff --git a/apps/workspace-engine/oapi/spec/schemas/core.jsonnet b/apps/workspace-engine/oapi/spec/schemas/core.jsonnet index 1e79cffb5..198bac179 100644 --- a/apps/workspace-engine/oapi/spec/schemas/core.jsonnet +++ b/apps/workspace-engine/oapi/spec/schemas/core.jsonnet @@ -95,6 +95,10 @@ local openapi = import '../lib/openapi.libsonnet'; items: { type: 'string' }, description: 'Optional provider-specific path components', }, + secretVersion: { + type: 'string', + description: 'Optional provider-specific version pin. For AWS Secrets Manager this maps to VersionId (uuid form) or VersionStage (AWSCURRENT/AWSPREVIOUS). For Doppler this maps to accept_secret_version. Empty means latest.', + }, }, }, diff --git a/apps/workspace-engine/pkg/db/convert.go b/apps/workspace-engine/pkg/db/convert.go index 5fa6fa553..4ae175f91 100644 --- a/apps/workspace-engine/pkg/db/convert.go +++ b/apps/workspace-engine/pkg/db/convert.go @@ -302,6 +302,7 @@ type VariableValueAggRow struct { SecretProvider *string `json:"secretProvider"` SecretKey *string `json:"secretKey"` SecretPath []string `json:"secretPath"` + SecretVersion *string `json:"secretVersion"` } func flattenVariableValue(r VariableValueAggRow) (oapi.Value, error) { @@ -332,6 +333,10 @@ func flattenVariableValue(r VariableValueAggRow) (oapi.Value, error) { path := append([]string(nil), r.SecretPath...) sr.SecretPath = &path } + if r.SecretVersion != nil && *r.SecretVersion != "" { + version := *r.SecretVersion + sr.SecretVersion = &version + } if err := v.FromSecretReferenceValue(sr); err != nil { return v, err } diff --git a/apps/workspace-engine/pkg/db/models.go b/apps/workspace-engine/pkg/db/models.go index 36962dbcf..713acfc6a 100644 --- a/apps/workspace-engine/pkg/db/models.go +++ b/apps/workspace-engine/pkg/db/models.go @@ -887,6 +887,7 @@ type VariableValue struct { SecretProvider pgtype.Text SecretKey pgtype.Text SecretPath []string + SecretVersion pgtype.Text CreatedAt pgtype.Timestamptz UpdatedAt pgtype.Timestamptz } diff --git a/apps/workspace-engine/pkg/db/queries/schema.sql b/apps/workspace-engine/pkg/db/queries/schema.sql index 756b1d6c4..0f8afd046 100644 --- a/apps/workspace-engine/pkg/db/queries/schema.sql +++ b/apps/workspace-engine/pkg/db/queries/schema.sql @@ -517,6 +517,7 @@ CREATE TABLE variable_value ( secret_provider TEXT, secret_key TEXT, secret_path TEXT[], + secret_version TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); diff --git a/apps/workspace-engine/pkg/db/queries/variables.sql b/apps/workspace-engine/pkg/db/queries/variables.sql index 701343f9f..077be2da8 100644 --- a/apps/workspace-engine/pkg/db/queries/variables.sql +++ b/apps/workspace-engine/pkg/db/queries/variables.sql @@ -22,7 +22,8 @@ SELECT 'refPath', vv.ref_path, 'secretProvider', vv.secret_provider, 'secretKey', vv.secret_key, - 'secretPath', vv.secret_path + 'secretPath', vv.secret_path, + 'secretVersion', vv.secret_version ) ORDER BY vv.priority DESC, vv.id ASC ) @@ -58,7 +59,8 @@ SELECT 'refPath', vv.ref_path, 'secretProvider', vv.secret_provider, 'secretKey', vv.secret_key, - 'secretPath', vv.secret_path + 'secretPath', vv.secret_path, + 'secretVersion', vv.secret_version ) ORDER BY vv.priority DESC, vv.id ASC ) @@ -91,7 +93,8 @@ SELECT 'refPath', vv.ref_path, 'secretProvider', vv.secret_provider, 'secretKey', vv.secret_key, - 'secretPath', vv.secret_path + 'secretPath', vv.secret_path, + 'secretVersion', vv.secret_version ) ORDER BY vv.priority DESC, vv.id ASC ) @@ -128,7 +131,8 @@ SELECT 'refPath', vv.ref_path, 'secretProvider', vv.secret_provider, 'secretKey', vv.secret_key, - 'secretPath', vv.secret_path + 'secretPath', vv.secret_path, + 'secretVersion', vv.secret_version ) ORDER BY vv.priority DESC, vv.id ASC ) diff --git a/apps/workspace-engine/pkg/db/variables.sql.go b/apps/workspace-engine/pkg/db/variables.sql.go index 867c80f6e..474a6219d 100644 --- a/apps/workspace-engine/pkg/db/variables.sql.go +++ b/apps/workspace-engine/pkg/db/variables.sql.go @@ -33,7 +33,8 @@ SELECT 'refPath', vv.ref_path, 'secretProvider', vv.secret_provider, 'secretKey', vv.secret_key, - 'secretPath', vv.secret_path + 'secretPath', vv.secret_path, + 'secretVersion', vv.secret_version ) ORDER BY vv.priority DESC, vv.id ASC ) @@ -107,7 +108,8 @@ SELECT 'refPath', vv.ref_path, 'secretProvider', vv.secret_provider, 'secretKey', vv.secret_key, - 'secretPath', vv.secret_path + 'secretPath', vv.secret_path, + 'secretVersion', vv.secret_version ) ORDER BY vv.priority DESC, vv.id ASC ) @@ -186,7 +188,8 @@ SELECT 'refPath', vv.ref_path, 'secretProvider', vv.secret_provider, 'secretKey', vv.secret_key, - 'secretPath', vv.secret_path + 'secretPath', vv.secret_path, + 'secretVersion', vv.secret_version ) ORDER BY vv.priority DESC, vv.id ASC ) @@ -265,7 +268,8 @@ SELECT 'refPath', vv.ref_path, 'secretProvider', vv.secret_provider, 'secretKey', vv.secret_key, - 'secretPath', vv.secret_path + 'secretPath', vv.secret_path, + 'secretVersion', vv.secret_version ) ORDER BY vv.priority DESC, vv.id ASC ) diff --git a/apps/workspace-engine/pkg/oapi/oapi.gen.go b/apps/workspace-engine/pkg/oapi/oapi.gen.go index 42aee3024..f49d0c86a 100644 --- a/apps/workspace-engine/pkg/oapi/oapi.gen.go +++ b/apps/workspace-engine/pkg/oapi/oapi.gen.go @@ -1169,6 +1169,9 @@ type SecretReferenceValue struct { // SecretProvider Workspace-unique secret_provider.name SecretProvider string `json:"secretProvider"` + + // SecretVersion Optional provider-specific version pin. For AWS Secrets Manager this maps to VersionId (uuid form) or VersionStage (AWSCURRENT/AWSPREVIOUS). For Doppler this maps to accept_secret_version. Empty means latest. + SecretVersion *string `json:"secretVersion,omitempty"` } // SensitiveValue defines model for SensitiveValue. diff --git a/apps/workspace-engine/pkg/secrets/awssm/provider.go b/apps/workspace-engine/pkg/secrets/awssm/provider.go index 38cfa9146..71d0a4fcb 100644 --- a/apps/workspace-engine/pkg/secrets/awssm/provider.go +++ b/apps/workspace-engine/pkg/secrets/awssm/provider.go @@ -14,6 +14,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "github.com/aws/aws-sdk-go-v2/aws" awsconfig "github.com/aws/aws-sdk-go-v2/config" @@ -96,6 +97,22 @@ func buildAWSConfig(ctx context.Context, cfg Config) (aws.Config, error) { func (*Provider) Type() string { return Type } +// isVersionStage returns true if version names a Secrets Manager +// VersionStage label rather than a VersionId. AWS-defined stages are +// AWSCURRENT, AWSPREVIOUS, and AWSPENDING; user-defined stages can be any +// label up to 64 characters but cannot start with "AWS" unless they are +// the AWS-defined ones above. We treat any value that starts with "AWS" +// (case-insensitive) as a stage; UUIDs and other arbitrary identifiers +// fall through to VersionId. Callers can also opt-in with a sentinel +// prefix "stage:" (e.g. "stage:my-custom-stage") for user-defined stages. +func isVersionStage(version string) bool { + const stagePrefix = "stage:" + if strings.HasPrefix(version, stagePrefix) { + return true + } + return strings.HasPrefix(strings.ToUpper(version), "AWS") +} + func (p *Provider) Resolve(ctx context.Context, ref secrets.SecretReference) (string, error) { if ref.Path == "" { return "", fmt.Errorf( @@ -103,9 +120,22 @@ func (p *Provider) Resolve(ctx context.Context, ref secrets.SecretReference) (st ) } - out, err := p.client.GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{ + input := &secretsmanager.GetSecretValueInput{ SecretId: aws.String(ref.Path), - }) + } + if ref.Version != "" { + // AWS Secrets Manager distinguishes VersionId (a UUID identifying a + // specific historical version) from VersionStage (a label like + // AWSCURRENT or AWSPREVIOUS). All-uppercase labels starting with + // "AWS" are treated as stages; everything else as a version id. + if isVersionStage(ref.Version) { + input.VersionStage = aws.String(strings.TrimPrefix(ref.Version, "stage:")) + } else { + input.VersionId = aws.String(ref.Version) + } + } + + out, err := p.client.GetSecretValue(ctx, input) if err != nil { return "", fmt.Errorf("awssm provider: GetSecretValue %s: %w", ref.Path, err) } diff --git a/apps/workspace-engine/pkg/secrets/awssm/provider_test.go b/apps/workspace-engine/pkg/secrets/awssm/provider_test.go index 75589e969..42472d5c3 100644 --- a/apps/workspace-engine/pkg/secrets/awssm/provider_test.go +++ b/apps/workspace-engine/pkg/secrets/awssm/provider_test.go @@ -58,6 +58,84 @@ func TestResolveReturnsRawSecretStringWhenKeyEmpty(t *testing.T) { } } +func TestResolveVersionId(t *testing.T) { + fc := &fakeClient{ + out: &secretsmanager.GetSecretValueOutput{SecretString: aws.String("v")}, + } + p := &Provider{client: fc} + + _, err := p.Resolve(context.Background(), secrets.SecretReference{ + Path: "prod/db", + Version: "ab12cd34-ef56-7890-abcd-ef1234567890", + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if fc.in.VersionId == nil || *fc.in.VersionId != "ab12cd34-ef56-7890-abcd-ef1234567890" { + t.Fatalf("expected VersionId to be set, got %+v", fc.in.VersionId) + } + if fc.in.VersionStage != nil { + t.Fatalf("VersionStage must not be set for a UUID version, got %q", *fc.in.VersionStage) + } +} + +func TestResolveVersionStageAWSCURRENT(t *testing.T) { + fc := &fakeClient{ + out: &secretsmanager.GetSecretValueOutput{SecretString: aws.String("v")}, + } + p := &Provider{client: fc} + + _, err := p.Resolve(context.Background(), secrets.SecretReference{ + Path: "prod/db", + Version: "AWSCURRENT", + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if fc.in.VersionStage == nil || *fc.in.VersionStage != "AWSCURRENT" { + t.Fatalf("expected VersionStage=AWSCURRENT, got %+v", fc.in.VersionStage) + } + if fc.in.VersionId != nil { + t.Fatalf("VersionId must not be set for a stage, got %q", *fc.in.VersionId) + } +} + +func TestResolveVersionUserStageWithPrefix(t *testing.T) { + fc := &fakeClient{ + out: &secretsmanager.GetSecretValueOutput{SecretString: aws.String("v")}, + } + p := &Provider{client: fc} + + _, err := p.Resolve(context.Background(), secrets.SecretReference{ + Path: "prod/db", + Version: "stage:custom-pin", + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if fc.in.VersionStage == nil || *fc.in.VersionStage != "custom-pin" { + t.Fatalf("expected VersionStage=custom-pin, got %+v", fc.in.VersionStage) + } +} + +func TestResolveNoVersionPassthrough(t *testing.T) { + fc := &fakeClient{ + out: &secretsmanager.GetSecretValueOutput{SecretString: aws.String("v")}, + } + p := &Provider{client: fc} + + _, err := p.Resolve(context.Background(), secrets.SecretReference{Path: "prod/db"}) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if fc.in.VersionId != nil { + t.Fatalf("VersionId must be nil when no version specified") + } + if fc.in.VersionStage != nil { + t.Fatalf("VersionStage must be nil when no version specified") + } +} + func TestResolveExtractsJSONFieldByKey(t *testing.T) { fc := &fakeClient{ out: &secretsmanager.GetSecretValueOutput{ diff --git a/apps/workspace-engine/pkg/secrets/cache.go b/apps/workspace-engine/pkg/secrets/cache.go index d7e092fce..2f1ec2aee 100644 --- a/apps/workspace-engine/pkg/secrets/cache.go +++ b/apps/workspace-engine/pkg/secrets/cache.go @@ -15,6 +15,7 @@ type cacheKey struct { Provider string Path string Key string + Version string } type cacheEntry struct { @@ -48,6 +49,7 @@ func keyFor(workspaceID uuid.UUID, ref SecretReference) cacheKey { Provider: ref.Provider, Path: ref.Path, Key: ref.Key, + Version: ref.Version, } } diff --git a/apps/workspace-engine/pkg/secrets/cache_test.go b/apps/workspace-engine/pkg/secrets/cache_test.go index 6e9dba3b2..28a21784f 100644 --- a/apps/workspace-engine/pkg/secrets/cache_test.go +++ b/apps/workspace-engine/pkg/secrets/cache_test.go @@ -84,6 +84,31 @@ func TestCacheInvalidateProvider(t *testing.T) { } } +func TestCacheKeysDistinguishVersions(t *testing.T) { + c := NewCache(time.Minute) + ws := uuid.New() + + c.Set(ws, SecretReference{Provider: "p", Path: "x", Key: "K", Version: ""}, "latest") + c.Set(ws, SecretReference{Provider: "p", Path: "x", Key: "K", Version: "v1"}, "pinned-v1") + c.Set(ws, SecretReference{Provider: "p", Path: "x", Key: "K", Version: "v2"}, "pinned-v2") + + if v, _ := c.Get(ws, SecretReference{Provider: "p", Path: "x", Key: "K"}); v != "latest" { + t.Fatalf("latest: got %q want latest", v) + } + if v, _ := c.Get( + ws, + SecretReference{Provider: "p", Path: "x", Key: "K", Version: "v1"}, + ); v != "pinned-v1" { + t.Fatalf("v1: got %q want pinned-v1", v) + } + if v, _ := c.Get( + ws, + SecretReference{Provider: "p", Path: "x", Key: "K", Version: "v2"}, + ); v != "pinned-v2" { + t.Fatalf("v2: got %q want pinned-v2", v) + } +} + func TestCacheKeysDistinguishPaths(t *testing.T) { c := NewCache(time.Minute) ws := uuid.New() diff --git a/apps/workspace-engine/pkg/secrets/doppler/provider.go b/apps/workspace-engine/pkg/secrets/doppler/provider.go index 6cb76a8a0..f53561182 100644 --- a/apps/workspace-engine/pkg/secrets/doppler/provider.go +++ b/apps/workspace-engine/pkg/secrets/doppler/provider.go @@ -82,6 +82,9 @@ func (p *Provider) Resolve(ctx context.Context, ref secrets.SecretReference) (st q.Set("project", project) q.Set("config", config) q.Set("name", ref.Key) + if ref.Version != "" { + q.Set("accept_secret_version", ref.Version) + } u.RawQuery = q.Encode() req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) diff --git a/apps/workspace-engine/pkg/secrets/doppler/provider_test.go b/apps/workspace-engine/pkg/secrets/doppler/provider_test.go index 553551374..14b76a9ad 100644 --- a/apps/workspace-engine/pkg/secrets/doppler/provider_test.go +++ b/apps/workspace-engine/pkg/secrets/doppler/provider_test.go @@ -90,6 +90,46 @@ func TestResolveEmptyValue(t *testing.T) { } } +func TestResolveVersionPin(t *testing.T) { + var captured string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + captured = r.URL.Query().Get("accept_secret_version") + _, _ = w.Write([]byte(`{"value":{"computed":"x","raw":"x"}}`)) + })) + defer srv.Close() + + p := newTestProvider(t, srv) + _, err := p.Resolve(context.Background(), secrets.SecretReference{ + Path: "backend/prod", + Key: "TOKEN", + Version: "42", + }) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if captured != "42" { + t.Fatalf("expected accept_secret_version=42, got %q", captured) + } +} + +func TestResolveNoVersionOmitsQuery(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Has("accept_secret_version") { + t.Errorf("accept_secret_version must not be present, got %v", r.URL.Query()) + } + _, _ = w.Write([]byte(`{"value":{"computed":"x","raw":"x"}}`)) + })) + defer srv.Close() + + p := newTestProvider(t, srv) + if _, err := p.Resolve( + context.Background(), + secrets.SecretReference{Path: "p/c", Key: "K"}, + ); err != nil { + t.Fatalf("Resolve: %v", err) + } +} + func TestResolveFallsBackToRaw(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(`{"value":{"computed":"","raw":"raw-val"}}`)) diff --git a/apps/workspace-engine/pkg/secrets/types.go b/apps/workspace-engine/pkg/secrets/types.go index fd5092a7c..e2438800e 100644 --- a/apps/workspace-engine/pkg/secrets/types.go +++ b/apps/workspace-engine/pkg/secrets/types.go @@ -21,6 +21,11 @@ type SecretReference struct { // Key identifies the secret within Path. Some providers ignore Path and // use Key alone (e.g. env). Key string + // Version optionally pins to a specific provider-side version. Empty + // means "latest" — awssm reads AWSCURRENT, Doppler the latest published + // version. When set: awssm uses VersionId (uuid form) or VersionStage + // (AWSCURRENT/AWSPREVIOUS), Doppler uses accept_secret_version. + Version string } // Provider resolves a SecretReference against an external secret store. diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/value.go b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/value.go index 804d68202..9e756a7a8 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/value.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/value.go @@ -106,6 +106,9 @@ func resolveSecretReference( ref.Path += "/" + (*srv.SecretPath)[i] } } + if srv.SecretVersion != nil { + ref.Version = *srv.SecretVersion + } plaintext, err := secretResolver.Resolve(ctx, workspaceID, ref) if err != nil { return nil, fmt.Errorf( diff --git a/packages/db/drizzle/0196_third_reavers.sql b/packages/db/drizzle/0196_third_reavers.sql new file mode 100644 index 000000000..ee2bb5ce9 --- /dev/null +++ b/packages/db/drizzle/0196_third_reavers.sql @@ -0,0 +1 @@ +ALTER TABLE "variable_value" ADD COLUMN "secret_version" text; \ No newline at end of file diff --git a/packages/db/drizzle/meta/0196_snapshot.json b/packages/db/drizzle/meta/0196_snapshot.json new file mode 100644 index 000000000..1e6cfa0b4 --- /dev/null +++ b/packages/db/drizzle/meta/0196_snapshot.json @@ -0,0 +1,7613 @@ +{ + "id": "d057e185-cd6f-46eb-8210-23cf3df48a31", + "prevId": "3efa7aef-401a-4c31-ad08-524dbe24d098", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "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 + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "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 + }, + "password": { + "name": "password", + "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": true, + "default": "now()" + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "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": true + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_session_token_unique": { + "name": "session_session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "session_token" + ] + } + }, + "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 + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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()" + }, + "updated_at": { + "name": "updated_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.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.changelog_entry": { + "name": "changelog_entry", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_data": { + "name": "entity_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "changelog_entry_workspace_id_workspace_id_fk": { + "name": "changelog_entry_workspace_id_workspace_id_fk", + "tableFrom": "changelog_entry", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "changelog_entry_workspace_id_entity_type_entity_id_pk": { + "name": "changelog_entry_workspace_id_entity_type_entity_id_pk", + "columns": [ + "workspace_id", + "entity_type", + "entity_id" + ] + } + }, + "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_plan": { + "name": "deployment_plan", + "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 + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version_tag": { + "name": "version_tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version_name": { + "name": "version_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version_config": { + "name": "version_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "version_job_agent_config": { + "name": "version_job_agent_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "version_metadata": { + "name": "version_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "deployment_plan_workspace_id_index": { + "name": "deployment_plan_workspace_id_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_plan_deployment_id_index": { + "name": "deployment_plan_deployment_id_index", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_plan_expires_at_index": { + "name": "deployment_plan_expires_at_index", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_plan_workspace_id_workspace_id_fk": { + "name": "deployment_plan_workspace_id_workspace_id_fk", + "tableFrom": "deployment_plan", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_plan_deployment_id_deployment_id_fk": { + "name": "deployment_plan_deployment_id_deployment_id_fk", + "tableFrom": "deployment_plan", + "tableTo": "deployment", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_plan_target": { + "name": "deployment_plan_target", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plan_id": { + "name": "plan_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "current_release_id": { + "name": "current_release_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "deployment_plan_target_plan_id_index": { + "name": "deployment_plan_target_plan_id_index", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_plan_target_plan_id_environment_id_resource_id_index": { + "name": "deployment_plan_target_plan_id_environment_id_resource_id_index", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_plan_target_plan_id_deployment_plan_id_fk": { + "name": "deployment_plan_target_plan_id_deployment_plan_id_fk", + "tableFrom": "deployment_plan_target", + "tableTo": "deployment_plan", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_plan_target_environment_id_environment_id_fk": { + "name": "deployment_plan_target_environment_id_environment_id_fk", + "tableFrom": "deployment_plan_target", + "tableTo": "environment", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_plan_target_resource_id_resource_id_fk": { + "name": "deployment_plan_target_resource_id_resource_id_fk", + "tableFrom": "deployment_plan_target", + "tableTo": "resource", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_plan_target_current_release_id_release_id_fk": { + "name": "deployment_plan_target_current_release_id_release_id_fk", + "tableFrom": "deployment_plan_target", + "tableTo": "release", + "columnsFrom": [ + "current_release_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_plan_target_result": { + "name": "deployment_plan_target_result", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "dispatch_context": { + "name": "dispatch_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "deployment_plan_target_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'computing'" + }, + "has_changes": { + "name": "has_changes", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current": { + "name": "current", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proposed": { + "name": "proposed", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "deployment_plan_target_result_target_id_index": { + "name": "deployment_plan_target_result_target_id_index", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_plan_target_result_target_id_deployment_plan_target_id_fk": { + "name": "deployment_plan_target_result_target_id_deployment_plan_target_id_fk", + "tableFrom": "deployment_plan_target_result", + "tableTo": "deployment_plan_target", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_plan_target_result_validation": { + "name": "deployment_plan_target_result_validation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "result_id": { + "name": "result_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "passed": { + "name": "passed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "violations": { + "name": "violations", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "evaluated_at": { + "name": "evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "deployment_plan_target_result_validation_result_id_rule_id_index": { + "name": "deployment_plan_target_result_validation_result_id_rule_id_index", + "columns": [ + { + "expression": "result_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_plan_target_result_validation_result_id_deployment_plan_target_result_id_fk": { + "name": "deployment_plan_target_result_validation_result_id_deployment_plan_target_result_id_fk", + "tableFrom": "deployment_plan_target_result_validation", + "tableTo": "deployment_plan_target_result", + "columnsFrom": [ + "result_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_plan_target_variable": { + "name": "deployment_plan_target_variable", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_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 + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "deployment_plan_target_variable_target_id_key_index": { + "name": "deployment_plan_target_variable_target_id_key_index", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_plan_target_variable_target_id_deployment_plan_target_id_fk": { + "name": "deployment_plan_target_variable_target_id_deployment_plan_target_id_fk", + "tableFrom": "deployment_plan_target_variable", + "tableTo": "deployment_plan_target", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_trace_span": { + "name": "deployment_trace_span", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "span_id": { + "name": "span_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_span_id": { + "name": "parent_span_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_time": { + "name": "start_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_time": { + "name": "end_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "release_target_key": { + "name": "release_target_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_id": { + "name": "release_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_trace_id": { + "name": "parent_trace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "node_type": { + "name": "node_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "depth": { + "name": "depth", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attributes": { + "name": "attributes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "events": { + "name": "events", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "deployment_trace_span_trace_span_idx": { + "name": "deployment_trace_span_trace_span_idx", + "columns": [ + { + "expression": "trace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "span_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_trace_id_idx": { + "name": "deployment_trace_span_trace_id_idx", + "columns": [ + { + "expression": "trace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_parent_span_id_idx": { + "name": "deployment_trace_span_parent_span_id_idx", + "columns": [ + { + "expression": "parent_span_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_workspace_id_idx": { + "name": "deployment_trace_span_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_release_target_key_idx": { + "name": "deployment_trace_span_release_target_key_idx", + "columns": [ + { + "expression": "release_target_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_release_id_idx": { + "name": "deployment_trace_span_release_id_idx", + "columns": [ + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_job_id_idx": { + "name": "deployment_trace_span_job_id_idx", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_parent_trace_id_idx": { + "name": "deployment_trace_span_parent_trace_id_idx", + "columns": [ + { + "expression": "parent_trace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_created_at_idx": { + "name": "deployment_trace_span_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_phase_idx": { + "name": "deployment_trace_span_phase_idx", + "columns": [ + { + "expression": "phase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_node_type_idx": { + "name": "deployment_trace_span_node_type_idx", + "columns": [ + { + "expression": "node_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployment_trace_span_status_idx": { + "name": "deployment_trace_span_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_trace_span_workspace_id_workspace_id_fk": { + "name": "deployment_trace_span_workspace_id_workspace_id_fk", + "tableFrom": "deployment_trace_span", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_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()" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_value": { + "name": "default_value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "deployment_variable_deployment_id_index": { + "name": "deployment_variable_deployment_id_index", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "deployment_variable_deployment_id_key_unique": { + "name": "deployment_variable_deployment_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "deployment_id", + "key" + ] + } + }, + "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()" + }, + "deployment_variable_id": { + "name": "deployment_variable_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "resource_selector": { + "name": "resource_selector", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "deployment_variable_value_deployment_variable_id_index": { + "name": "deployment_variable_value_deployment_variable_id_index", + "columns": [ + { + "expression": "deployment_variable_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_variable_value_deployment_variable_id_deployment_variable_id_fk": { + "name": "deployment_variable_value_deployment_variable_id_deployment_variable_id_fk", + "tableFrom": "deployment_variable_value", + "tableTo": "deployment_variable", + "columnsFrom": [ + "deployment_variable_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "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_workspace_id_workspace_id_fk": { + "name": "deployment_version_workspace_id_workspace_id_fk", + "tableFrom": "deployment_version", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_version_dependency": { + "name": "deployment_version_dependency", + "schema": "", + "columns": { + "deployment_version_id": { + "name": "deployment_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "dependency_deployment_id": { + "name": "dependency_deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version_selector": { + "name": "version_selector", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'false'" + } + }, + "indexes": { + "deployment_version_dependency_target_idx": { + "name": "deployment_version_dependency_target_idx", + "columns": [ + { + "expression": "dependency_deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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_dependency_deployment_id_deployment_id_fk": { + "name": "deployment_version_dependency_dependency_deployment_id_deployment_id_fk", + "tableFrom": "deployment_version_dependency", + "tableTo": "deployment", + "columnsFrom": [ + "dependency_deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "deployment_version_dependency_deployment_version_id_dependency_deployment_id_pk": { + "name": "deployment_version_dependency_deployment_version_id_dependency_deployment_id_pk", + "columns": [ + "deployment_version_id", + "dependency_deployment_id" + ] + } + }, + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_evaluated_at": { + "name": "last_evaluated_at", + "type": "timestamp with time zone", + "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 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_selector": { + "name": "resource_selector", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'false'" + }, + "job_agent_selector": { + "name": "job_agent_selector", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'false'" + }, + "job_agent_config": { + "name": "job_agent_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "deployment_workspace_id_workspace_id_fk": { + "name": "deployment_workspace_id_workspace_id_fk", + "tableFrom": "deployment", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "deployment_workspace_id_name_unique": { + "name": "deployment_workspace_id_name_unique", + "nullsNotDistinct": false, + "columns": [ + "workspace_id", + "name" + ] + } + }, + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_evaluated_at": { + "name": "last_evaluated_at", + "type": "timestamp with time zone", + "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()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "resource_selector": { + "name": "resource_selector", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'false'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "environment_workspace_id_workspace_id_fk": { + "name": "environment_workspace_id_workspace_id_fk", + "tableFrom": "environment", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_workspace_id_name_unique": { + "name": "environment_workspace_id_name_unique", + "nullsNotDistinct": false, + "columns": [ + "workspace_id", + "name" + ] + } + }, + "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.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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "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": {} + }, + "resource_workspace_id_active_idx": { + "name": "resource_workspace_id_active_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resource_workspace_id_deleted_at_index": { + "name": "resource_workspace_id_deleted_at_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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_aggregate": { + "name": "resource_aggregate", + "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": 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 + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "filter": { + "name": "filter", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'true'" + }, + "group_by": { + "name": "group_by", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resource_aggregate_workspace_id_index": { + "name": "resource_aggregate_workspace_id_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_aggregate_workspace_id_workspace_id_fk": { + "name": "resource_aggregate_workspace_id_workspace_id_fk", + "tableFrom": "resource_aggregate", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "resource_aggregate_created_by_user_id_fk": { + "name": "resource_aggregate_created_by_user_id_fk", + "tableFrom": "resource_aggregate", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "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_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 with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "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": "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 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": { + "system_workspace_id_index": { + "name": "system_workspace_id_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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.system_deployment": { + "name": "system_deployment", + "schema": "", + "columns": { + "system_id": { + "name": "system_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "deployment_id": { + "name": "deployment_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": { + "system_deployment_system_id_system_id_fk": { + "name": "system_deployment_system_id_system_id_fk", + "tableFrom": "system_deployment", + "tableTo": "system", + "columnsFrom": [ + "system_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "system_deployment_deployment_id_deployment_id_fk": { + "name": "system_deployment_deployment_id_deployment_id_fk", + "tableFrom": "system_deployment", + "tableTo": "deployment", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "system_deployment_system_id_deployment_id_pk": { + "name": "system_deployment_system_id_deployment_id_pk", + "columns": [ + "system_id", + "deployment_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_environment": { + "name": "system_environment", + "schema": "", + "columns": { + "system_id": { + "name": "system_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_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": { + "system_environment_system_id_system_id_fk": { + "name": "system_environment_system_id_system_id_fk", + "tableFrom": "system_environment", + "tableTo": "system", + "columnsFrom": [ + "system_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "system_environment_environment_id_environment_id_fk": { + "name": "system_environment_environment_id_environment_id_fk", + "tableFrom": "system_environment", + "tableTo": "environment", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "system_environment_system_id_environment_id_pk": { + "name": "system_environment_system_id_environment_id_pk", + "columns": [ + "system_id", + "environment_id" + ] + } + }, + "uniqueConstraints": {}, + "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 + }, + "trace_token": { + "name": "trace_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dispatch_context": { + "name": "dispatch_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "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": {} + }, + "job_external_id_idx": { + "name": "job_external_id_idx", + "columns": [ + { + "expression": "external_id", + "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": {} + }, + "job_metadata_job_id_idx": { + "name": "job_metadata_job_id_idx", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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 + }, + "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.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.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.release": { + "name": "release", + "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 + }, + "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": { + "release_resource_id_environment_id_deployment_id_index": { + "name": "release_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": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "release_deployment_id_index": { + "name": "release_deployment_id_index", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "release_resource_id_resource_id_fk": { + "name": "release_resource_id_resource_id_fk", + "tableFrom": "release", + "tableTo": "resource", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_environment_id_environment_id_fk": { + "name": "release_environment_id_environment_id_fk", + "tableFrom": "release", + "tableTo": "environment", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_deployment_id_deployment_id_fk": { + "name": "release_deployment_id_deployment_id_fk", + "tableFrom": "release", + "tableTo": "deployment", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_version_id_deployment_version_id_fk": { + "name": "release_version_id_deployment_version_id_fk", + "tableFrom": "release", + "tableTo": "deployment_version", + "columnsFrom": [ + "version_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()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "release_id": { + "name": "release_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "release_job_release_id_job_id_index": { + "name": "release_job_release_id_job_id_index", + "columns": [ + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "release_job_job_id_index": { + "name": "release_job_job_id_index", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "release_job_release_id_index": { + "name": "release_job_release_id_index", + "columns": [ + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "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" + }, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.release_target_desired_release": { + "name": "release_target_desired_release", + "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 + } + }, + "indexes": { + "release_target_desired_release_resource_id_environment_id_deployment_id_index": { + "name": "release_target_desired_release_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_desired_release_resource_id_resource_id_fk": { + "name": "release_target_desired_release_resource_id_resource_id_fk", + "tableFrom": "release_target_desired_release", + "tableTo": "resource", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_target_desired_release_environment_id_environment_id_fk": { + "name": "release_target_desired_release_environment_id_environment_id_fk", + "tableFrom": "release_target_desired_release", + "tableTo": "environment", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_target_desired_release_deployment_id_deployment_id_fk": { + "name": "release_target_desired_release_deployment_id_deployment_id_fk", + "tableFrom": "release_target_desired_release", + "tableTo": "deployment", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "release_target_desired_release_desired_release_id_release_id_fk": { + "name": "release_target_desired_release_desired_release_id_release_id_fk", + "tableFrom": "release_target_desired_release", + "tableTo": "release", + "columnsFrom": [ + "desired_release_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.release_variable": { + "name": "release_variable", + "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 + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "encrypted": { + "name": "encrypted", + "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": { + "release_variable_release_id_key_index": { + "name": "release_variable_release_id_key_index", + "columns": [ + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "release_variable_release_id_release_id_fk": { + "name": "release_variable_release_id_release_id_fk", + "tableFrom": "release_variable", + "tableTo": "release", + "columnsFrom": [ + "release_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reconcile_work_scope": { + "name": "reconcile_work_scope", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "reconcile_work_scope_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "event_ts": { + "name": "event_ts", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "priority": { + "name": "priority", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "not_before": { + "name": "not_before", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_until": { + "name": "claimed_until", + "type": "timestamp with time zone", + "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": true, + "default": "now()" + } + }, + "indexes": { + "reconcile_work_scope_workspace_id_kind_scope_type_scope_id_index": { + "name": "reconcile_work_scope_workspace_id_kind_scope_type_scope_id_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "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": {} + }, + "reconcile_work_scope_unclaimed_idx": { + "name": "reconcile_work_scope_unclaimed_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_ts", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"reconcile_work_scope\".\"claimed_until\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "reconcile_work_scope_expired_claims_idx": { + "name": "reconcile_work_scope_expired_claims_idx", + "columns": [ + { + "expression": "claimed_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"reconcile_work_scope\".\"claimed_until\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "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 + }, + "selector": { + "name": "selector", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'true'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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()" + } + }, + "indexes": { + "policy_workspace_id_index": { + "name": "policy_workspace_id_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": {}, + "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 + }, + "min_approvals": { + "name": "min_approvals", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "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_deployment_dependency": { + "name": "policy_rule_deployment_dependency", + "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 + }, + "depends_on": { + "name": "depends_on", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_deployment_dependency_policy_id_policy_id_fk": { + "name": "policy_rule_deployment_dependency_policy_id_policy_id_fk", + "tableFrom": "policy_rule_deployment_dependency", + "tableTo": "policy", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_deployment_window": { + "name": "policy_rule_deployment_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 + }, + "allow_window": { + "name": "allow_window", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "duration_minutes": { + "name": "duration_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rrule": { + "name": "rrule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_deployment_window_policy_id_policy_id_fk": { + "name": "policy_rule_deployment_window_policy_id_policy_id_fk", + "tableFrom": "policy_rule_deployment_window", + "tableTo": "policy", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_environment_progression": { + "name": "policy_rule_environment_progression", + "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 + }, + "depends_on_environment_selector": { + "name": "depends_on_environment_selector", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "maximum_age_hours": { + "name": "maximum_age_hours", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "minimum_soak_time_minutes": { + "name": "minimum_soak_time_minutes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "minimum_success_percentage": { + "name": "minimum_success_percentage", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "success_statuses": { + "name": "success_statuses", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "require_verification_passed": { + "name": "require_verification_passed", + "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": {}, + "foreignKeys": { + "policy_rule_environment_progression_policy_id_policy_id_fk": { + "name": "policy_rule_environment_progression_policy_id_policy_id_fk", + "tableFrom": "policy_rule_environment_progression", + "tableTo": "policy", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_gradual_rollout": { + "name": "policy_rule_gradual_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 + }, + "rollout_type": { + "name": "rollout_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time_scale_interval": { + "name": "time_scale_interval", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_gradual_rollout_policy_id_policy_id_fk": { + "name": "policy_rule_gradual_rollout_policy_id_policy_id_fk", + "tableFrom": "policy_rule_gradual_rollout", + "tableTo": "policy", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_plan_validation_opa": { + "name": "policy_rule_plan_validation_opa", + "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 + }, + "rego": { + "name": "rego", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "policy_rule_plan_validation_opa_policy_id_index": { + "name": "policy_rule_plan_validation_opa_policy_id_index", + "columns": [ + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "policy_rule_plan_validation_opa_policy_id_policy_id_fk": { + "name": "policy_rule_plan_validation_opa_policy_id_policy_id_fk", + "tableFrom": "policy_rule_plan_validation_opa", + "tableTo": "policy", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_retry": { + "name": "policy_rule_retry", + "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 + }, + "max_retries": { + "name": "max_retries", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "backoff_seconds": { + "name": "backoff_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "backoff_strategy": { + "name": "backoff_strategy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_backoff_seconds": { + "name": "max_backoff_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "retry_on_statuses": { + "name": "retry_on_statuses", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_retry_policy_id_policy_id_fk": { + "name": "policy_rule_retry_policy_id_policy_id_fk", + "tableFrom": "policy_rule_retry", + "tableTo": "policy", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_rollback": { + "name": "policy_rule_rollback", + "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 + }, + "on_job_statuses": { + "name": "on_job_statuses", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "on_verification_failure": { + "name": "on_verification_failure", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_rollback_policy_id_policy_id_fk": { + "name": "policy_rule_rollback_policy_id_policy_id_fk", + "tableFrom": "policy_rule_rollback", + "tableTo": "policy", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_verification": { + "name": "policy_rule_verification", + "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 + }, + "metrics": { + "name": "metrics", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "trigger_on": { + "name": "trigger_on", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_verification_policy_id_policy_id_fk": { + "name": "policy_rule_verification_policy_id_policy_id_fk", + "tableFrom": "policy_rule_verification", + "tableTo": "policy", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_version_cooldown": { + "name": "policy_rule_version_cooldown", + "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 + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_version_cooldown_policy_id_policy_id_fk": { + "name": "policy_rule_version_cooldown_policy_id_policy_id_fk", + "tableFrom": "policy_rule_version_cooldown", + "tableTo": "policy", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_version_selector": { + "name": "policy_rule_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 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "selector": { + "name": "selector", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_version_selector_policy_id_policy_id_fk": { + "name": "policy_rule_version_selector_policy_id_policy_id_fk", + "tableFrom": "policy_rule_version_selector", + "tableTo": "policy", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_approval_record": { + "name": "user_approval_record", + "schema": "", + "columns": { + "version_id": { + "name": "version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_approval_record_version_id_user_id_environment_id_pk": { + "name": "user_approval_record_version_id_user_id_environment_id_pk", + "columns": [ + "version_id", + "user_id", + "environment_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_variable": { + "name": "resource_variable", + "schema": "", + "columns": { + "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": true + } + }, + "indexes": {}, + "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": { + "resource_variable_resource_id_key_pk": { + "name": "resource_variable_resource_id_key_pk", + "columns": [ + "resource_id", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "job_agents": { + "name": "job_agents", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_job": { + "name": "workflow_job", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workflow_run_id": { + "name": "workflow_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_job_workflow_run_id_workflow_run_id_fk": { + "name": "workflow_job_workflow_run_id_workflow_run_id_fk", + "tableFrom": "workflow_job", + "tableTo": "workflow_run", + "columnsFrom": [ + "workflow_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_job_job_id_job_id_fk": { + "name": "workflow_job_job_id_job_id_fk", + "tableFrom": "workflow_job", + "tableTo": "job", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_run": { + "name": "workflow_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workflow_id": { + "name": "workflow_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_run_workflow_id_workflow_id_fk": { + "name": "workflow_run_workflow_id_workflow_id_fk", + "tableFrom": "workflow_run", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_skip": { + "name": "policy_skip", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "rule_id": { + "name": "rule_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version_id": { + "name": "version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.computed_policy_release_target": { + "name": "computed_policy_release_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 + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "computed_at": { + "name": "computed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "computed_policy_release_target_policy_id_environment_id_deployment_id_resource_id_index": { + "name": "computed_policy_release_target_policy_id_environment_id_deployment_id_resource_id_index", + "columns": [ + { + "expression": "policy_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" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "computed_policy_release_target_policy_id_index": { + "name": "computed_policy_release_target_policy_id_index", + "columns": [ + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "computed_policy_release_target_resource_id_environment_id_deployment_id_index": { + "name": "computed_policy_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": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "computed_policy_release_target_policy_id_policy_id_fk": { + "name": "computed_policy_release_target_policy_id_policy_id_fk", + "tableFrom": "computed_policy_release_target", + "tableTo": "policy", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "computed_policy_release_target_environment_id_environment_id_fk": { + "name": "computed_policy_release_target_environment_id_environment_id_fk", + "tableFrom": "computed_policy_release_target", + "tableTo": "environment", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "computed_policy_release_target_deployment_id_deployment_id_fk": { + "name": "computed_policy_release_target_deployment_id_deployment_id_fk", + "tableFrom": "computed_policy_release_target", + "tableTo": "deployment", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "computed_policy_release_target_resource_id_resource_id_fk": { + "name": "computed_policy_release_target_resource_id_resource_id_fk", + "tableFrom": "computed_policy_release_target", + "tableTo": "resource", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_evaluation": { + "name": "policy_rule_evaluation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "rule_type": { + "name": "rule_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version_id": { + "name": "version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "allowed": { + "name": "allowed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "action_required": { + "name": "action_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "satisfied_at": { + "name": "satisfied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_evaluation_at": { + "name": "next_evaluation_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "evaluated_at": { + "name": "evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "policy_rule_evaluation_rule_id_environment_id_version_id_resource_id_index": { + "name": "policy_rule_evaluation_rule_id_environment_id_version_id_resource_id_index", + "columns": [ + { + "expression": "rule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "policy_rule_evaluation_environment_id_version_id_resource_id_rule_type_index": { + "name": "policy_rule_evaluation_environment_id_version_id_resource_id_rule_type_index", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rule_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "policy_rule_evaluation_environment_id_environment_id_fk": { + "name": "policy_rule_evaluation_environment_id_environment_id_fk", + "tableFrom": "policy_rule_evaluation", + "tableTo": "environment", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "policy_rule_evaluation_version_id_deployment_version_id_fk": { + "name": "policy_rule_evaluation_version_id_deployment_version_id_fk", + "tableFrom": "policy_rule_evaluation", + "tableTo": "deployment_version", + "columnsFrom": [ + "version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "policy_rule_evaluation_resource_id_resource_id_fk": { + "name": "policy_rule_evaluation_resource_id_resource_id_fk", + "tableFrom": "policy_rule_evaluation", + "tableTo": "resource", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_verification_metric_measurement": { + "name": "job_verification_metric_measurement", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_verification_metric_status_id": { + "name": "job_verification_metric_status_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "measured_at": { + "name": "measured_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "job_verification_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "job_verification_metric_measurement_job_verification_metric_status_id_index": { + "name": "job_verification_metric_measurement_job_verification_metric_status_id_index", + "columns": [ + { + "expression": "job_verification_metric_status_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_verification_metric_measurement_job_verification_metric_status_id_job_verification_metric_id_fk": { + "name": "job_verification_metric_measurement_job_verification_metric_status_id_job_verification_metric_id_fk", + "tableFrom": "job_verification_metric_measurement", + "tableTo": "job_verification_metric", + "columnsFrom": [ + "job_verification_metric_status_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_verification_metric": { + "name": "job_verification_metric", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "policy_rule_verification_metric_id": { + "name": "policy_rule_verification_metric_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "success_threshold": { + "name": "success_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "failure_condition": { + "name": "failure_condition", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'false'" + }, + "failure_threshold": { + "name": "failure_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + } + }, + "indexes": { + "job_verification_metric_job_id_index": { + "name": "job_verification_metric_job_id_index", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_verification_metric_policy_rule_verification_metric_id_index": { + "name": "job_verification_metric_policy_rule_verification_metric_id_index", + "columns": [ + { + "expression": "policy_rule_verification_metric_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_verification_metric_policy_rule_verification_metric_id_policy_rule_job_verification_metric_id_fk": { + "name": "job_verification_metric_policy_rule_verification_metric_id_policy_rule_job_verification_metric_id_fk", + "tableFrom": "job_verification_metric", + "tableTo": "policy_rule_job_verification_metric", + "columnsFrom": [ + "policy_rule_verification_metric_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_rule_job_verification_metric": { + "name": "policy_rule_job_verification_metric", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "trigger_on": { + "name": "trigger_on", + "type": "job_verification_trigger_on", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'jobSuccess'" + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "success_threshold": { + "name": "success_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "failure_condition": { + "name": "failure_condition", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'false'" + }, + "failure_threshold": { + "name": "failure_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "policy_rule_job_verification_metric_policy_id_policy_id_fk": { + "name": "policy_rule_job_verification_metric_policy_id_policy_id_fk", + "tableFrom": "policy_rule_job_verification_metric", + "tableTo": "policy", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.computed_entity_relationship": { + "name": "computed_entity_relationship", + "schema": "", + "columns": { + "rule_id": { + "name": "rule_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "from_entity_type": { + "name": "from_entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_entity_id": { + "name": "from_entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "to_entity_type": { + "name": "to_entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_entity_id": { + "name": "to_entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "last_evaluated_at": { + "name": "last_evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "computed_entity_relationship_from_idx": { + "name": "computed_entity_relationship_from_idx", + "columns": [ + { + "expression": "from_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "from_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "computed_entity_relationship_to_idx": { + "name": "computed_entity_relationship_to_idx", + "columns": [ + { + "expression": "to_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "to_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "computed_entity_relationship_rule_id_relationship_rule_id_fk": { + "name": "computed_entity_relationship_rule_id_relationship_rule_id_fk", + "tableFrom": "computed_entity_relationship", + "tableTo": "relationship_rule", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "computed_entity_relationship_rule_id_from_entity_type_from_entity_id_to_entity_type_to_entity_id_pk": { + "name": "computed_entity_relationship_rule_id_from_entity_type_from_entity_id_to_entity_type_to_entity_id_pk", + "columns": [ + "rule_id", + "from_entity_type", + "from_entity_id", + "to_entity_type", + "to_entity_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.relationship_rule": { + "name": "relationship_rule", + "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": true + }, + "reference": { + "name": "reference", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cel": { + "name": "cel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + } + }, + "indexes": { + "relationship_rule_workspace_id_reference_index": { + "name": "relationship_rule_workspace_id_reference_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reference", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "relationship_rule_workspace_id_index": { + "name": "relationship_rule_workspace_id_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "relationship_rule_workspace_id_workspace_id_fk": { + "name": "relationship_rule_workspace_id_workspace_id_fk", + "tableFrom": "relationship_rule", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_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": "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": true, + "default": "''" + }, + "selector": { + "name": "selector", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "variable_set_workspace_id_workspace_id_fk": { + "name": "variable_set_workspace_id_workspace_id_fk", + "tableFrom": "variable_set", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.variable_set_variable": { + "name": "variable_set_variable", + "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 + } + }, + "indexes": {}, + "foreignKeys": { + "variable_set_variable_variable_set_id_variable_set_id_fk": { + "name": "variable_set_variable_variable_set_id_variable_set_id_fk", + "tableFrom": "variable_set_variable", + "tableTo": "variable_set", + "columnsFrom": [ + "variable_set_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "variable_set_variable_variable_set_id_key_unique": { + "name": "variable_set_variable_variable_set_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "variable_set_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.variable": { + "name": "variable", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scope": { + "name": "scope", + "type": "variable_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "job_agent_id": { + "name": "job_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_sensitive": { + "name": "is_sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "description": { + "name": "description", + "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": true, + "default": "now()" + } + }, + "indexes": { + "variable_resource_key_uniq": { + "name": "variable_resource_key_uniq", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"variable\".\"resource_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "variable_deployment_key_uniq": { + "name": "variable_deployment_key_uniq", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"variable\".\"deployment_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "variable_job_agent_key_uniq": { + "name": "variable_job_agent_key_uniq", + "columns": [ + { + "expression": "job_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"variable\".\"job_agent_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "variable_scope_idx": { + "name": "variable_scope_idx", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "variable_resource_id_resource_id_fk": { + "name": "variable_resource_id_resource_id_fk", + "tableFrom": "variable", + "tableTo": "resource", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "variable_deployment_id_deployment_id_fk": { + "name": "variable_deployment_id_deployment_id_fk", + "tableFrom": "variable", + "tableTo": "deployment", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "variable_job_agent_id_job_agent_id_fk": { + "name": "variable_job_agent_id_job_agent_id_fk", + "tableFrom": "variable", + "tableTo": "job_agent", + "columnsFrom": [ + "job_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "variable_scope_target_check": { + "name": "variable_scope_target_check", + "value": "\n (\n \"variable\".\"scope\" = 'resource'\n and \"variable\".\"resource_id\" is not null\n and \"variable\".\"deployment_id\" is null\n and \"variable\".\"job_agent_id\" is null\n )\n or\n (\n \"variable\".\"scope\" = 'deployment'\n and \"variable\".\"deployment_id\" is not null\n and \"variable\".\"resource_id\" is null\n and \"variable\".\"job_agent_id\" is null\n )\n or\n (\n \"variable\".\"scope\" = 'job_agent'\n and \"variable\".\"job_agent_id\" is not null\n and \"variable\".\"resource_id\" is null\n and \"variable\".\"deployment_id\" is null\n )\n " + } + }, + "isRLSEnabled": false + }, + "public.variable_value": { + "name": "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": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "kind": { + "name": "kind", + "type": "variable_value_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "literal_value": { + "name": "literal_value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ref_key": { + "name": "ref_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ref_path": { + "name": "ref_path", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "secret_provider": { + "name": "secret_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_key": { + "name": "secret_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_path": { + "name": "secret_path", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "secret_version": { + "name": "secret_version", + "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": true, + "default": "now()" + } + }, + "indexes": { + "variable_value_variable_priority_idx": { + "name": "variable_value_variable_priority_idx", + "columns": [ + { + "expression": "variable_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "variable_value_kind_idx": { + "name": "variable_value_kind_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "variable_value_resolution_uniq": { + "name": "variable_value_resolution_uniq", + "columns": [ + { + "expression": "variable_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"resource_selector\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "variable_value_variable_id_variable_id_fk": { + "name": "variable_value_variable_id_variable_id_fk", + "tableFrom": "variable_value", + "tableTo": "variable", + "columnsFrom": [ + "variable_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "variable_value_kind_shape_check": { + "name": "variable_value_kind_shape_check", + "value": "\n (\n \"variable_value\".\"kind\" = 'literal'\n and \"variable_value\".\"literal_value\" is not null\n and \"variable_value\".\"ref_key\" is null\n and \"variable_value\".\"ref_path\" is null\n and \"variable_value\".\"secret_provider\" is null\n and \"variable_value\".\"secret_key\" is null\n and \"variable_value\".\"secret_path\" is null\n )\n or\n (\n \"variable_value\".\"kind\" = 'ref'\n and \"variable_value\".\"literal_value\" is null\n and \"variable_value\".\"ref_key\" is not null\n and \"variable_value\".\"secret_provider\" is null\n and \"variable_value\".\"secret_key\" is null\n and \"variable_value\".\"secret_path\" is null\n )\n or\n (\n \"variable_value\".\"kind\" = 'secret_ref'\n and \"variable_value\".\"literal_value\" is null\n and \"variable_value\".\"ref_key\" is null\n and \"variable_value\".\"ref_path\" is null\n and \"variable_value\".\"secret_provider\" is not null\n and \"variable_value\".\"secret_key\" is not null\n )\n " + } + }, + "isRLSEnabled": false + }, + "public.secret_provider": { + "name": "secret_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 + }, + "type": { + "name": "type", + "type": "secret_provider_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "bytea", + "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": true, + "default": "now()" + } + }, + "indexes": { + "secret_provider_workspace_name_uniq": { + "name": "secret_provider_workspace_name_uniq", + "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": { + "secret_provider_workspace_id_workspace_id_fk": { + "name": "secret_provider_workspace_id_workspace_id_fk", + "tableFrom": "secret_provider", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_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_plan_target_status": { + "name": "deployment_plan_target_status", + "schema": "public", + "values": [ + "computing", + "completed", + "errored", + "unsupported" + ] + }, + "public.deployment_version_status": { + "name": "deployment_version_status", + "schema": "public", + "values": [ + "unspecified", + "building", + "ready", + "failed", + "rejected", + "paused" + ] + }, + "public.job_reason": { + "name": "job_reason", + "schema": "public", + "values": [ + "policy_passing", + "policy_override", + "env_policy_override", + "config_policy_override", + "redeploy" + ] + }, + "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", + "resource", + "resourceProvider", + "workspace", + "environment", + "system", + "deployment" + ] + }, + "public.job_verification_status": { + "name": "job_verification_status", + "schema": "public", + "values": [ + "failed", + "inconclusive", + "passed" + ] + }, + "public.job_verification_trigger_on": { + "name": "job_verification_trigger_on", + "schema": "public", + "values": [ + "jobCreated", + "jobStarted", + "jobSuccess", + "jobFailure" + ] + }, + "public.variable_scope": { + "name": "variable_scope", + "schema": "public", + "values": [ + "resource", + "deployment", + "job_agent" + ] + }, + "public.variable_value_kind": { + "name": "variable_value_kind", + "schema": "public", + "values": [ + "literal", + "ref", + "secret_ref" + ] + }, + "public.secret_provider_type": { + "name": "secret_provider_type", + "schema": "public", + "values": [ + "aws_secrets_manager", + "doppler", + "env" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 1b2af7ada..325f32474 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -1373,6 +1373,13 @@ "when": 1778514718456, "tag": "0195_unique_cobalt_man", "breakpoints": true + }, + { + "idx": 196, + "version": "7", + "when": 1778763395165, + "tag": "0196_third_reavers", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/variable.ts b/packages/db/src/schema/variable.ts index f1db98736..194f73bad 100644 --- a/packages/db/src/schema/variable.ts +++ b/packages/db/src/schema/variable.ts @@ -128,6 +128,7 @@ export const variableValue = pgTable( secretProvider: text("secret_provider"), secretKey: text("secret_key"), secretPath: text("secret_path").array(), + secretVersion: text("secret_version"), createdAt: timestamp("created_at", { withTimezone: true }) .notNull() From a6992df6f8067a8022bd8140e0328f8aa03d5858 Mon Sep 17 00:00:00 2001 From: Michael Leone Date: Thu, 14 May 2026 09:50:55 -0400 Subject: [PATCH 11/13] fix(golangci_lint): fix linting --- .../workspace-engine/pkg/jobagents/argo/argocd_plan.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/workspace-engine/pkg/jobagents/argo/argocd_plan.go b/apps/workspace-engine/pkg/jobagents/argo/argocd_plan.go index 43c74d138..455324f72 100644 --- a/apps/workspace-engine/pkg/jobagents/argo/argocd_plan.go +++ b/apps/workspace-engine/pkg/jobagents/argo/argocd_plan.go @@ -36,11 +36,11 @@ func normalizeApplicationForDiff(app *v1alpha1.Application) *v1alpha1.Applicatio APIVersion: "argoproj.io/v1alpha1", Kind: "Application", } - cp.ObjectMeta.ManagedFields = nil - cp.ObjectMeta.CreationTimestamp = metav1.Time{} - cp.ObjectMeta.Generation = 0 - cp.ObjectMeta.ResourceVersion = "" - cp.ObjectMeta.UID = "" + cp.ManagedFields = nil + cp.CreationTimestamp = metav1.Time{} + cp.Generation = 0 + cp.ResourceVersion = "" + cp.UID = "" cp.Status = v1alpha1.ApplicationStatus{} cp.Operation = nil return cp From 10c128a8bbb528e469c50d42bf30343346c3a0b7 Mon Sep 17 00:00:00 2001 From: Michael Leone Date: Thu, 14 May 2026 10:08:16 -0400 Subject: [PATCH 12/13] fix(db): lint --- packages/db/drizzle/meta/0196_snapshot.json | 940 +++++--------------- packages/db/drizzle/meta/_journal.json | 2 +- packages/db/src/schema/secret-provider.ts | 2 +- 3 files changed, 230 insertions(+), 714 deletions(-) diff --git a/packages/db/drizzle/meta/0196_snapshot.json b/packages/db/drizzle/meta/0196_snapshot.json index 052cded05..f561484a4 100644 --- a/packages/db/drizzle/meta/0196_snapshot.json +++ b/packages/db/drizzle/meta/0196_snapshot.json @@ -112,12 +112,8 @@ "name": "account_user_id_user_id_fk", "tableFrom": "account", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -205,12 +201,8 @@ "name": "session_user_id_user_id_fk", "tableFrom": "session", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -220,9 +212,7 @@ "session_session_token_unique": { "name": "session_session_token_unique", "nullsNotDistinct": false, - "columns": [ - "session_token" - ] + "columns": ["session_token"] } }, "policies": {}, @@ -308,12 +298,8 @@ "name": "user_active_workspace_id_workspace_id_fk", "tableFrom": "user", "tableTo": "workspace", - "columnsFrom": [ - "active_workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["active_workspace_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -400,12 +386,8 @@ "name": "user_api_key_user_id_user_id_fk", "tableFrom": "user_api_key", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -525,12 +507,8 @@ "name": "changelog_entry_workspace_id_workspace_id_fk", "tableFrom": "changelog_entry", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -538,11 +516,7 @@ "compositePrimaryKeys": { "changelog_entry_workspace_id_entity_type_entity_id_pk": { "name": "changelog_entry_workspace_id_entity_type_entity_id_pk", - "columns": [ - "workspace_id", - "entity_type", - "entity_id" - ] + "columns": ["workspace_id", "entity_type", "entity_id"] } }, "uniqueConstraints": {}, @@ -599,12 +573,8 @@ "name": "dashboard_workspace_id_workspace_id_fk", "tableFrom": "dashboard", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -683,12 +653,8 @@ "name": "dashboard_widget_dashboard_id_dashboard_id_fk", "tableFrom": "dashboard_widget", "tableTo": "dashboard", - "columnsFrom": [ - "dashboard_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["dashboard_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -834,12 +800,8 @@ "name": "deployment_plan_workspace_id_workspace_id_fk", "tableFrom": "deployment_plan", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -847,12 +809,8 @@ "name": "deployment_plan_deployment_id_deployment_id_fk", "tableFrom": "deployment_plan", "tableTo": "deployment", - "columnsFrom": [ - "deployment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -948,12 +906,8 @@ "name": "deployment_plan_target_plan_id_deployment_plan_id_fk", "tableFrom": "deployment_plan_target", "tableTo": "deployment_plan", - "columnsFrom": [ - "plan_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["plan_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -961,12 +915,8 @@ "name": "deployment_plan_target_environment_id_environment_id_fk", "tableFrom": "deployment_plan_target", "tableTo": "environment", - "columnsFrom": [ - "environment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -974,12 +924,8 @@ "name": "deployment_plan_target_resource_id_resource_id_fk", "tableFrom": "deployment_plan_target", "tableTo": "resource", - "columnsFrom": [ - "resource_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -987,12 +933,8 @@ "name": "deployment_plan_target_current_release_id_release_id_fk", "tableFrom": "deployment_plan_target", "tableTo": "release", - "columnsFrom": [ - "current_release_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["current_release_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -1106,12 +1048,8 @@ "name": "deployment_plan_target_result_target_id_deployment_plan_target_id_fk", "tableFrom": "deployment_plan_target_result", "tableTo": "deployment_plan_target", - "columnsFrom": [ - "target_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["target_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1194,12 +1132,8 @@ "name": "deployment_plan_target_result_validation_result_id_deployment_plan_target_result_id_fk", "tableFrom": "deployment_plan_target_result_validation", "tableTo": "deployment_plan_target_result", - "columnsFrom": [ - "result_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["result_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1275,12 +1209,8 @@ "name": "deployment_plan_target_variable_target_id_deployment_plan_target_id_fk", "tableFrom": "deployment_plan_target_variable", "tableTo": "deployment_plan_target", - "columnsFrom": [ - "target_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["target_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1611,12 +1541,8 @@ "name": "deployment_trace_span_workspace_id_workspace_id_fk", "tableFrom": "deployment_trace_span", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1685,12 +1611,8 @@ "name": "deployment_variable_deployment_id_deployment_id_fk", "tableFrom": "deployment_variable", "tableTo": "deployment", - "columnsFrom": [ - "deployment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1700,10 +1622,7 @@ "deployment_variable_deployment_id_key_unique": { "name": "deployment_variable_deployment_id_key_unique", "nullsNotDistinct": false, - "columns": [ - "deployment_id", - "key" - ] + "columns": ["deployment_id", "key"] } }, "policies": {}, @@ -1769,12 +1688,8 @@ "name": "deployment_variable_value_deployment_variable_id_deployment_variable_id_fk", "tableFrom": "deployment_variable_value", "tableTo": "deployment_variable", - "columnsFrom": [ - "deployment_variable_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_variable_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1906,12 +1821,8 @@ "name": "deployment_version_workspace_id_workspace_id_fk", "tableFrom": "deployment_version", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1968,12 +1879,8 @@ "name": "deployment_version_dependency_deployment_version_id_deployment_version_id_fk", "tableFrom": "deployment_version_dependency", "tableTo": "deployment_version", - "columnsFrom": [ - "deployment_version_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1981,12 +1888,8 @@ "name": "deployment_version_dependency_dependency_deployment_id_deployment_id_fk", "tableFrom": "deployment_version_dependency", "tableTo": "deployment", - "columnsFrom": [ - "dependency_deployment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["dependency_deployment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1994,10 +1897,7 @@ "compositePrimaryKeys": { "deployment_version_dependency_deployment_version_id_dependency_deployment_id_pk": { "name": "deployment_version_dependency_deployment_version_id_dependency_deployment_id_pk", - "columns": [ - "deployment_version_id", - "dependency_deployment_id" - ] + "columns": ["deployment_version_id", "dependency_deployment_id"] } }, "uniqueConstraints": {}, @@ -2041,12 +1941,8 @@ "name": "computed_deployment_resource_deployment_id_deployment_id_fk", "tableFrom": "computed_deployment_resource", "tableTo": "deployment", - "columnsFrom": [ - "deployment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2054,12 +1950,8 @@ "name": "computed_deployment_resource_resource_id_resource_id_fk", "tableFrom": "computed_deployment_resource", "tableTo": "resource", - "columnsFrom": [ - "resource_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2067,10 +1959,7 @@ "compositePrimaryKeys": { "computed_deployment_resource_deployment_id_resource_id_pk": { "name": "computed_deployment_resource_deployment_id_resource_id_pk", - "columns": [ - "deployment_id", - "resource_id" - ] + "columns": ["deployment_id", "resource_id"] } }, "uniqueConstraints": {}, @@ -2142,12 +2031,8 @@ "name": "deployment_workspace_id_workspace_id_fk", "tableFrom": "deployment", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -2157,10 +2042,7 @@ "deployment_workspace_id_name_unique": { "name": "deployment_workspace_id_name_unique", "nullsNotDistinct": false, - "columns": [ - "workspace_id", - "name" - ] + "columns": ["workspace_id", "name"] } }, "policies": {}, @@ -2203,12 +2085,8 @@ "name": "computed_environment_resource_environment_id_environment_id_fk", "tableFrom": "computed_environment_resource", "tableTo": "environment", - "columnsFrom": [ - "environment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2216,12 +2094,8 @@ "name": "computed_environment_resource_resource_id_resource_id_fk", "tableFrom": "computed_environment_resource", "tableTo": "resource", - "columnsFrom": [ - "resource_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2229,10 +2103,7 @@ "compositePrimaryKeys": { "computed_environment_resource_environment_id_resource_id_pk": { "name": "computed_environment_resource_environment_id_resource_id_pk", - "columns": [ - "environment_id", - "resource_id" - ] + "columns": ["environment_id", "resource_id"] } }, "uniqueConstraints": {}, @@ -2298,12 +2169,8 @@ "name": "environment_workspace_id_workspace_id_fk", "tableFrom": "environment", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -2313,10 +2180,7 @@ "environment_workspace_id_name_unique": { "name": "environment_workspace_id_name_unique", "nullsNotDistinct": false, - "columns": [ - "workspace_id", - "name" - ] + "columns": ["workspace_id", "name"] } }, "policies": {}, @@ -2366,12 +2230,8 @@ "name": "event_workspace_id_workspace_id_fk", "tableFrom": "event", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2527,12 +2387,8 @@ "name": "resource_provider_id_resource_provider_id_fk", "tableFrom": "resource", "tableTo": "resource_provider", - "columnsFrom": [ - "provider_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["provider_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -2540,12 +2396,8 @@ "name": "resource_workspace_id_workspace_id_fk", "tableFrom": "resource", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2640,12 +2492,8 @@ "name": "resource_aggregate_workspace_id_workspace_id_fk", "tableFrom": "resource_aggregate", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -2653,12 +2501,8 @@ "name": "resource_aggregate_created_by_user_id_fk", "tableFrom": "resource_aggregate", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -2739,12 +2583,8 @@ "name": "resource_schema_workspace_id_workspace_id_fk", "tableFrom": "resource_schema", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -2821,12 +2661,8 @@ "name": "resource_provider_workspace_id_workspace_id_fk", "tableFrom": "resource_provider", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2897,12 +2733,8 @@ "name": "system_workspace_id_workspace_id_fk", "tableFrom": "system", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2943,12 +2775,8 @@ "name": "system_deployment_system_id_system_id_fk", "tableFrom": "system_deployment", "tableTo": "system", - "columnsFrom": [ - "system_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["system_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2956,12 +2784,8 @@ "name": "system_deployment_deployment_id_deployment_id_fk", "tableFrom": "system_deployment", "tableTo": "deployment", - "columnsFrom": [ - "deployment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2969,10 +2793,7 @@ "compositePrimaryKeys": { "system_deployment_system_id_deployment_id_pk": { "name": "system_deployment_system_id_deployment_id_pk", - "columns": [ - "system_id", - "deployment_id" - ] + "columns": ["system_id", "deployment_id"] } }, "uniqueConstraints": {}, @@ -3010,12 +2831,8 @@ "name": "system_environment_system_id_system_id_fk", "tableFrom": "system_environment", "tableTo": "system", - "columnsFrom": [ - "system_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["system_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3023,12 +2840,8 @@ "name": "system_environment_environment_id_environment_id_fk", "tableFrom": "system_environment", "tableTo": "environment", - "columnsFrom": [ - "environment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3036,10 +2849,7 @@ "compositePrimaryKeys": { "system_environment_system_id_environment_id_pk": { "name": "system_environment_system_id_environment_id_pk", - "columns": [ - "system_id", - "environment_id" - ] + "columns": ["system_id", "environment_id"] } }, "uniqueConstraints": {}, @@ -3077,12 +2887,8 @@ "name": "team_workspace_id_workspace_id_fk", "tableFrom": "team", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3145,12 +2951,8 @@ "name": "team_member_team_id_team_id_fk", "tableFrom": "team_member", "tableTo": "team", - "columnsFrom": [ - "team_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["team_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3158,12 +2960,8 @@ "name": "team_member_user_id_user_id_fk", "tableFrom": "team_member", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3318,12 +3116,8 @@ "name": "job_job_agent_id_job_agent_id_fk", "tableFrom": "job", "tableTo": "job_agent", - "columnsFrom": [ - "job_agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["job_agent_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -3407,12 +3201,8 @@ "name": "job_metadata_job_id_job_id_fk", "tableFrom": "job_metadata", "tableTo": "job", - "columnsFrom": [ - "job_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["job_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3488,12 +3278,8 @@ "name": "job_variable_job_id_job_id_fk", "tableFrom": "job_variable", "tableTo": "job", - "columnsFrom": [ - "job_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["job_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3542,9 +3328,7 @@ "workspace_slug_unique": { "name": "workspace_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] } }, "policies": {}, @@ -3635,12 +3419,8 @@ "name": "workspace_email_domain_matching_workspace_id_workspace_id_fk", "tableFrom": "workspace_email_domain_matching", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3648,12 +3428,8 @@ "name": "workspace_email_domain_matching_role_id_role_id_fk", "tableFrom": "workspace_email_domain_matching", "tableTo": "role", - "columnsFrom": [ - "role_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["role_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3713,12 +3489,8 @@ "name": "workspace_invite_token_role_id_role_id_fk", "tableFrom": "workspace_invite_token", "tableTo": "role", - "columnsFrom": [ - "role_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["role_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3726,12 +3498,8 @@ "name": "workspace_invite_token_workspace_id_workspace_id_fk", "tableFrom": "workspace_invite_token", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3739,12 +3507,8 @@ "name": "workspace_invite_token_created_by_user_id_fk", "tableFrom": "workspace_invite_token", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3754,9 +3518,7 @@ "workspace_invite_token_token_unique": { "name": "workspace_invite_token_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -3853,12 +3615,8 @@ "name": "entity_role_role_id_role_id_fk", "tableFrom": "entity_role", "tableTo": "role", - "columnsFrom": [ - "role_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["role_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3905,12 +3663,8 @@ "name": "role_workspace_id_workspace_id_fk", "tableFrom": "role", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3973,12 +3727,8 @@ "name": "role_permission_role_id_role_id_fk", "tableFrom": "role_permission", "tableTo": "role", - "columnsFrom": [ - "role_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["role_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4081,12 +3831,8 @@ "name": "release_resource_id_resource_id_fk", "tableFrom": "release", "tableTo": "resource", - "columnsFrom": [ - "resource_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4094,12 +3840,8 @@ "name": "release_environment_id_environment_id_fk", "tableFrom": "release", "tableTo": "environment", - "columnsFrom": [ - "environment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4107,12 +3849,8 @@ "name": "release_deployment_id_deployment_id_fk", "tableFrom": "release", "tableTo": "deployment", - "columnsFrom": [ - "deployment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4120,12 +3858,8 @@ "name": "release_version_id_deployment_version_id_fk", "tableFrom": "release", "tableTo": "deployment_version", - "columnsFrom": [ - "version_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["version_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4218,12 +3952,8 @@ "name": "release_job_job_id_job_id_fk", "tableFrom": "release_job", "tableTo": "job", - "columnsFrom": [ - "job_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["job_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4231,12 +3961,8 @@ "name": "release_job_release_id_release_id_fk", "tableFrom": "release_job", "tableTo": "release", - "columnsFrom": [ - "release_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["release_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4317,12 +4043,8 @@ "name": "release_target_desired_release_resource_id_resource_id_fk", "tableFrom": "release_target_desired_release", "tableTo": "resource", - "columnsFrom": [ - "resource_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4330,12 +4052,8 @@ "name": "release_target_desired_release_environment_id_environment_id_fk", "tableFrom": "release_target_desired_release", "tableTo": "environment", - "columnsFrom": [ - "environment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4343,12 +4061,8 @@ "name": "release_target_desired_release_deployment_id_deployment_id_fk", "tableFrom": "release_target_desired_release", "tableTo": "deployment", - "columnsFrom": [ - "deployment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4356,12 +4070,8 @@ "name": "release_target_desired_release_desired_release_id_release_id_fk", "tableFrom": "release_target_desired_release", "tableTo": "release", - "columnsFrom": [ - "desired_release_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["desired_release_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -4444,12 +4154,8 @@ "name": "release_variable_release_id_release_id_fk", "tableFrom": "release_variable", "tableTo": "release", - "columnsFrom": [ - "release_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["release_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4747,12 +4453,8 @@ "name": "policy_workspace_id_workspace_id_fk", "tableFrom": "policy", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4800,12 +4502,8 @@ "name": "policy_rule_any_approval_policy_id_policy_id_fk", "tableFrom": "policy_rule_any_approval", "tableTo": "policy", - "columnsFrom": [ - "policy_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4853,12 +4551,8 @@ "name": "policy_rule_deployment_dependency_policy_id_policy_id_fk", "tableFrom": "policy_rule_deployment_dependency", "tableTo": "policy", - "columnsFrom": [ - "policy_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4924,12 +4618,8 @@ "name": "policy_rule_deployment_window_policy_id_policy_id_fk", "tableFrom": "policy_rule_deployment_window", "tableTo": "policy", - "columnsFrom": [ - "policy_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5008,12 +4698,8 @@ "name": "policy_rule_environment_progression_policy_id_policy_id_fk", "tableFrom": "policy_rule_environment_progression", "tableTo": "policy", - "columnsFrom": [ - "policy_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5067,12 +4753,8 @@ "name": "policy_rule_gradual_rollout_policy_id_policy_id_fk", "tableFrom": "policy_rule_gradual_rollout", "tableTo": "policy", - "columnsFrom": [ - "policy_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5148,12 +4830,8 @@ "name": "policy_rule_plan_validation_opa_policy_id_policy_id_fk", "tableFrom": "policy_rule_plan_validation_opa", "tableTo": "policy", - "columnsFrom": [ - "policy_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5225,12 +4903,8 @@ "name": "policy_rule_retry_policy_id_policy_id_fk", "tableFrom": "policy_rule_retry", "tableTo": "policy", - "columnsFrom": [ - "policy_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5284,12 +4958,8 @@ "name": "policy_rule_rollback_policy_id_policy_id_fk", "tableFrom": "policy_rule_rollback", "tableTo": "policy", - "columnsFrom": [ - "policy_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5344,12 +5014,8 @@ "name": "policy_rule_verification_policy_id_policy_id_fk", "tableFrom": "policy_rule_verification", "tableTo": "policy", - "columnsFrom": [ - "policy_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5397,12 +5063,8 @@ "name": "policy_rule_version_cooldown_policy_id_policy_id_fk", "tableFrom": "policy_rule_version_cooldown", "tableTo": "policy", - "columnsFrom": [ - "policy_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5456,12 +5118,8 @@ "name": "policy_rule_version_selector_policy_id_policy_id_fk", "tableFrom": "policy_rule_version_selector", "tableTo": "policy", - "columnsFrom": [ - "policy_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5519,11 +5177,7 @@ "compositePrimaryKeys": { "user_approval_record_version_id_user_id_environment_id_pk": { "name": "user_approval_record_version_id_user_id_environment_id_pk", - "columns": [ - "version_id", - "user_id", - "environment_id" - ] + "columns": ["version_id", "user_id", "environment_id"] } }, "uniqueConstraints": {}, @@ -5560,12 +5214,8 @@ "name": "resource_variable_resource_id_resource_id_fk", "tableFrom": "resource_variable", "tableTo": "resource", - "columnsFrom": [ - "resource_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5573,10 +5223,7 @@ "compositePrimaryKeys": { "resource_variable_resource_id_key_pk": { "name": "resource_variable_resource_id_key_pk", - "columns": [ - "resource_id", - "key" - ] + "columns": ["resource_id", "key"] } }, "uniqueConstraints": {}, @@ -5634,12 +5281,8 @@ "name": "workflow_workspace_id_workspace_id_fk", "tableFrom": "workflow", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5689,12 +5332,8 @@ "name": "workflow_job_workflow_run_id_workflow_run_id_fk", "tableFrom": "workflow_job", "tableTo": "workflow_run", - "columnsFrom": [ - "workflow_run_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_run_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5702,12 +5341,8 @@ "name": "workflow_job_job_id_job_id_fk", "tableFrom": "workflow_job", "tableTo": "job", - "columnsFrom": [ - "job_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["job_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5749,12 +5384,8 @@ "name": "workflow_run_workflow_id_workflow_id_fk", "tableFrom": "workflow_run", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5961,12 +5592,8 @@ "name": "computed_policy_release_target_policy_id_policy_id_fk", "tableFrom": "computed_policy_release_target", "tableTo": "policy", - "columnsFrom": [ - "policy_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5974,12 +5601,8 @@ "name": "computed_policy_release_target_environment_id_environment_id_fk", "tableFrom": "computed_policy_release_target", "tableTo": "environment", - "columnsFrom": [ - "environment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5987,12 +5610,8 @@ "name": "computed_policy_release_target_deployment_id_deployment_id_fk", "tableFrom": "computed_policy_release_target", "tableTo": "deployment", - "columnsFrom": [ - "deployment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6000,12 +5619,8 @@ "name": "computed_policy_release_target_resource_id_resource_id_fk", "tableFrom": "computed_policy_release_target", "tableTo": "resource", - "columnsFrom": [ - "resource_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6182,12 +5797,8 @@ "name": "policy_rule_evaluation_environment_id_environment_id_fk", "tableFrom": "policy_rule_evaluation", "tableTo": "environment", - "columnsFrom": [ - "environment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6195,12 +5806,8 @@ "name": "policy_rule_evaluation_version_id_deployment_version_id_fk", "tableFrom": "policy_rule_evaluation", "tableTo": "deployment_version", - "columnsFrom": [ - "version_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["version_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6208,12 +5815,8 @@ "name": "policy_rule_evaluation_resource_id_resource_id_fk", "tableFrom": "policy_rule_evaluation", "tableTo": "resource", - "columnsFrom": [ - "resource_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6292,12 +5895,8 @@ "name": "job_verification_metric_measurement_job_verification_metric_status_id_job_verification_metric_id_fk", "tableFrom": "job_verification_metric_measurement", "tableTo": "job_verification_metric", - "columnsFrom": [ - "job_verification_metric_status_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["job_verification_metric_status_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6427,12 +6026,8 @@ "name": "job_verification_metric_policy_rule_verification_metric_id_policy_rule_job_verification_metric_id_fk", "tableFrom": "job_verification_metric", "tableTo": "policy_rule_job_verification_metric", - "columnsFrom": [ - "policy_rule_verification_metric_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_rule_verification_metric_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -6526,12 +6121,8 @@ "name": "policy_rule_job_verification_metric_policy_id_policy_id_fk", "tableFrom": "policy_rule_job_verification_metric", "tableTo": "policy", - "columnsFrom": [ - "policy_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["policy_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6633,12 +6224,8 @@ "name": "computed_entity_relationship_rule_id_relationship_rule_id_fk", "tableFrom": "computed_entity_relationship", "tableTo": "relationship_rule", - "columnsFrom": [ - "rule_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["rule_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6752,12 +6339,8 @@ "name": "relationship_rule_workspace_id_workspace_id_fk", "tableFrom": "relationship_rule", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6833,12 +6416,8 @@ "name": "job_agent_workspace_id_workspace_id_fk", "tableFrom": "job_agent", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6920,12 +6499,8 @@ "name": "variable_set_workspace_id_workspace_id_fk", "tableFrom": "variable_set", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6972,12 +6547,8 @@ "name": "variable_set_variable_variable_set_id_variable_set_id_fk", "tableFrom": "variable_set_variable", "tableTo": "variable_set", - "columnsFrom": [ - "variable_set_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["variable_set_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6987,10 +6558,7 @@ "variable_set_variable_variable_set_id_key_unique": { "name": "variable_set_variable_variable_set_id_key_unique", "nullsNotDistinct": false, - "columns": [ - "variable_set_id", - "key" - ] + "columns": ["variable_set_id", "key"] } }, "policies": {}, @@ -7155,12 +6723,8 @@ "name": "variable_resource_id_resource_id_fk", "tableFrom": "variable", "tableTo": "resource", - "columnsFrom": [ - "resource_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["resource_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7168,12 +6732,8 @@ "name": "variable_deployment_id_deployment_id_fk", "tableFrom": "variable", "tableTo": "deployment", - "columnsFrom": [ - "deployment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7181,12 +6741,8 @@ "name": "variable_job_agent_id_job_agent_id_fk", "tableFrom": "variable", "tableTo": "job_agent", - "columnsFrom": [ - "job_agent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["job_agent_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7372,12 +6928,8 @@ "name": "variable_value_variable_id_variable_id_fk", "tableFrom": "variable_value", "tableTo": "variable", - "columnsFrom": [ - "variable_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["variable_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7472,12 +7024,8 @@ "name": "secret_provider_workspace_id_workspace_id_fk", "tableFrom": "secret_provider", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7493,20 +7041,12 @@ "public.system_role": { "name": "system_role", "schema": "public", - "values": [ - "user", - "admin" - ] + "values": ["user", "admin"] }, "public.deployment_plan_target_status": { "name": "deployment_plan_target_status", "schema": "public", - "values": [ - "computing", - "completed", - "errored", - "unsupported" - ] + "values": ["computing", "completed", "errored", "unsupported"] }, "public.deployment_version_status": { "name": "deployment_version_status", @@ -7550,10 +7090,7 @@ "public.entity_type": { "name": "entity_type", "schema": "public", - "values": [ - "user", - "team" - ] + "values": ["user", "team"] }, "public.scope_type": { "name": "scope_type", @@ -7571,48 +7108,27 @@ "public.job_verification_status": { "name": "job_verification_status", "schema": "public", - "values": [ - "failed", - "inconclusive", - "passed" - ] + "values": ["failed", "inconclusive", "passed"] }, "public.job_verification_trigger_on": { "name": "job_verification_trigger_on", "schema": "public", - "values": [ - "jobCreated", - "jobStarted", - "jobSuccess", - "jobFailure" - ] + "values": ["jobCreated", "jobStarted", "jobSuccess", "jobFailure"] }, "public.variable_scope": { "name": "variable_scope", "schema": "public", - "values": [ - "resource", - "deployment", - "job_agent" - ] + "values": ["resource", "deployment", "job_agent"] }, "public.variable_value_kind": { "name": "variable_value_kind", "schema": "public", - "values": [ - "literal", - "ref", - "secret_ref" - ] + "values": ["literal", "ref", "secret_ref"] }, "public.secret_provider_type": { "name": "secret_provider_type", "schema": "public", - "values": [ - "aws_secrets_manager", - "doppler", - "env" - ] + "values": ["aws_secrets_manager", "doppler", "env"] } }, "schemas": {}, @@ -7625,4 +7141,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index bb33fe5ec..81eb86bae 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -1382,4 +1382,4 @@ "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/packages/db/src/schema/secret-provider.ts b/packages/db/src/schema/secret-provider.ts index 72f013d5c..a24b28187 100644 --- a/packages/db/src/schema/secret-provider.ts +++ b/packages/db/src/schema/secret-provider.ts @@ -1,6 +1,6 @@ import type { InferSelectModel } from "drizzle-orm"; -import { customType } from "drizzle-orm/pg-core"; import { + customType, pgEnum, pgTable, text, From d9693be569bdb833e43d035ccb8dbc4b3904bde4 Mon Sep 17 00:00:00 2001 From: Michael Leone Date: Fri, 15 May 2026 15:42:21 -0400 Subject: [PATCH 13/13] fix: lint --- packages/db/drizzle/meta/0196_snapshot.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/db/drizzle/meta/0196_snapshot.json b/packages/db/drizzle/meta/0196_snapshot.json index f561484a4..4352eb0f1 100644 --- a/packages/db/drizzle/meta/0196_snapshot.json +++ b/packages/db/drizzle/meta/0196_snapshot.json @@ -5292,10 +5292,7 @@ "workflow_workspace_id_slug_unique": { "name": "workflow_workspace_id_slug_unique", "nullsNotDistinct": false, - "columns": [ - "workspace_id", - "slug" - ] + "columns": ["workspace_id", "slug"] } }, "policies": {},