From 1ad5bf8a3750350fdd585db280935cb26ed61233 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Fri, 6 Mar 2026 13:11:47 -0800 Subject: [PATCH 01/11] init --- apps/workspace-engine/main.go | 2 + .../pkg/reconcile/events/policysummary.go | 98 ++++++++ .../releasemanager/policy/evaluators.go | 29 +++ .../controllers/policysummary/controller.go | 95 ++++++++ .../svc/controllers/policysummary/getters.go | 22 ++ .../policysummary/getters_postgres.go | 39 +++ .../controllers/policysummary/reconcile.go | 224 ++++++++++++++++++ .../svc/controllers/policysummary/scope.go | 62 +++++ .../svc/controllers/policysummary/setters.go | 25 ++ .../policysummary/setters_postgres.go | 20 ++ packages/db/src/schema/index.ts | 1 + packages/db/src/schema/policy-rule-summary.ts | 82 +++++++ 12 files changed, 699 insertions(+) create mode 100644 apps/workspace-engine/pkg/reconcile/events/policysummary.go create mode 100644 apps/workspace-engine/svc/controllers/policysummary/controller.go create mode 100644 apps/workspace-engine/svc/controllers/policysummary/getters.go create mode 100644 apps/workspace-engine/svc/controllers/policysummary/getters_postgres.go create mode 100644 apps/workspace-engine/svc/controllers/policysummary/reconcile.go create mode 100644 apps/workspace-engine/svc/controllers/policysummary/scope.go create mode 100644 apps/workspace-engine/svc/controllers/policysummary/setters.go create mode 100644 apps/workspace-engine/svc/controllers/policysummary/setters_postgres.go create mode 100644 packages/db/src/schema/policy-rule-summary.ts diff --git a/apps/workspace-engine/main.go b/apps/workspace-engine/main.go index 0be0103f4..a2149ba38 100644 --- a/apps/workspace-engine/main.go +++ b/apps/workspace-engine/main.go @@ -11,6 +11,7 @@ import ( "workspace-engine/svc" "workspace-engine/svc/controllers/deploymentresourceselectoreval" "workspace-engine/svc/controllers/desiredrelease" + "workspace-engine/svc/controllers/policysummary" "workspace-engine/svc/controllers/environmentresourceselectoreval" "workspace-engine/svc/controllers/jobdispatch" "workspace-engine/svc/controllers/jobverificationmetric" @@ -67,6 +68,7 @@ func main() { jobverificationmetric.New(WorkerID, db.GetPool(ctx)), relationshipeval.New(WorkerID, db.GetPool(ctx)), desiredrelease.New(WorkerID, db.GetPool(ctx)), + policysummary.New(WorkerID, db.GetPool(ctx)), ) if err := runner.Run(ctx); err != nil { diff --git a/apps/workspace-engine/pkg/reconcile/events/policysummary.go b/apps/workspace-engine/pkg/reconcile/events/policysummary.go new file mode 100644 index 000000000..3ccbb743b --- /dev/null +++ b/apps/workspace-engine/pkg/reconcile/events/policysummary.go @@ -0,0 +1,98 @@ +package events + +import ( + "context" + "fmt" + "workspace-engine/pkg/reconcile" + + "github.com/charmbracelet/log" +) + +const PolicySummaryKind = "policy-summary" + +const ( + PolicySummaryScopeEnvironment = "environment" + PolicySummaryScopeEnvironmentVersion = "environment-version" + PolicySummaryScopeDeploymentVersion = "deployment-version" +) + +type PolicySummaryParams struct { + WorkspaceID string + ScopeType string + ScopeID string +} + +type EnvironmentSummaryParams struct { + WorkspaceID string + EnvironmentID string +} + +func (p EnvironmentSummaryParams) ToParams() PolicySummaryParams { + return PolicySummaryParams{ + WorkspaceID: p.WorkspaceID, + ScopeType: PolicySummaryScopeEnvironment, + ScopeID: p.EnvironmentID, + } +} + +type EnvironmentVersionSummaryParams struct { + WorkspaceID string + EnvironmentID string + VersionID string +} + +func (p EnvironmentVersionSummaryParams) ScopeID() string { + return fmt.Sprintf("%s:%s", p.EnvironmentID, p.VersionID) +} + +func (p EnvironmentVersionSummaryParams) ToParams() PolicySummaryParams { + return PolicySummaryParams{ + WorkspaceID: p.WorkspaceID, + ScopeType: PolicySummaryScopeEnvironmentVersion, + ScopeID: p.ScopeID(), + } +} + +type DeploymentVersionSummaryParams struct { + WorkspaceID string + DeploymentID string + VersionID string +} + +func (p DeploymentVersionSummaryParams) ScopeID() string { + return fmt.Sprintf("%s:%s", p.DeploymentID, p.VersionID) +} + +func (p DeploymentVersionSummaryParams) ToParams() PolicySummaryParams { + return PolicySummaryParams{ + WorkspaceID: p.WorkspaceID, + ScopeType: PolicySummaryScopeDeploymentVersion, + ScopeID: p.ScopeID(), + } +} + +func EnqueuePolicySummary(queue reconcile.Queue, ctx context.Context, params PolicySummaryParams) error { + return queue.Enqueue(ctx, reconcile.EnqueueParams{ + WorkspaceID: params.WorkspaceID, + Kind: PolicySummaryKind, + ScopeType: params.ScopeType, + ScopeID: params.ScopeID, + }) +} + +func EnqueueManyPolicySummary(queue reconcile.Queue, ctx context.Context, params []PolicySummaryParams) error { + if len(params) == 0 { + return nil + } + log.Info("enqueueing policy summary", "count", len(params)) + items := make([]reconcile.EnqueueParams, len(params)) + for i, p := range params { + items[i] = reconcile.EnqueueParams{ + WorkspaceID: p.WorkspaceID, + Kind: PolicySummaryKind, + ScopeType: p.ScopeType, + ScopeID: p.ScopeID, + } + } + return queue.EnqueueMany(ctx, items) +} diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluators.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluators.go index 81577024a..f2762391b 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluators.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluators.go @@ -36,3 +36,32 @@ func EvaluatorsForSummary(store *store.Store, rule *oapi.PolicyRule) []evaluator versioncooldown.NewSummaryEvaluatorFromStore(store, rule), ) } + +// EvaluatorsForEnvironmentSummary returns evaluators scoped to an environment +// (no version/resource/deployment needed). Used by the policy-summary controller +// for the "environment" scope channel. +func EvaluatorsForEnvironmentSummary(store *store.Store, rule *oapi.PolicyRule) []evaluator.Evaluator { + return evaluator.CollectEvaluators( + deploymentwindow.NewSummaryEvaluatorFromStore(store, rule), + ) +} + +// EvaluatorsForEnvironmentVersionSummary returns evaluators scoped to an +// (environment, version) pair. Used by the policy-summary controller for the +// "environment-version" scope channel. +func EvaluatorsForEnvironmentVersionSummary(store *store.Store, rule *oapi.PolicyRule) []evaluator.Evaluator { + return evaluator.CollectEvaluators( + approval.NewEvaluatorFromStore(store, rule), + environmentprogression.NewEvaluatorFromStore(store, rule), + gradualrollout.NewSummaryEvaluatorFromStore(store, rule), + ) +} + +// EvaluatorsForDeploymentVersionSummary returns evaluators scoped to a +// (deployment, version) pair. Used by the policy-summary controller for the +// "deployment-version" scope channel. +func EvaluatorsForDeploymentVersionSummary(store *store.Store, rule *oapi.PolicyRule) []evaluator.Evaluator { + return evaluator.CollectEvaluators( + versioncooldown.NewSummaryEvaluatorFromStore(store, rule), + ) +} diff --git a/apps/workspace-engine/svc/controllers/policysummary/controller.go b/apps/workspace-engine/svc/controllers/policysummary/controller.go new file mode 100644 index 000000000..71ed97ae1 --- /dev/null +++ b/apps/workspace-engine/svc/controllers/policysummary/controller.go @@ -0,0 +1,95 @@ +package policysummary + +import ( + "context" + "fmt" + "runtime" + "time" + "workspace-engine/svc" + + "github.com/charmbracelet/log" + + "workspace-engine/pkg/reconcile" + "workspace-engine/pkg/reconcile/events" + "workspace-engine/pkg/reconcile/postgres" + + "github.com/jackc/pgx/v5/pgxpool" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" +) + +var tracer = otel.Tracer("workspace-engine/svc/controllers/policysummary") +var _ reconcile.Processor = (*Controller)(nil) + +type Controller struct { + getter Getter + setter Setter +} + +func (c *Controller) Process(ctx context.Context, item reconcile.Item) (reconcile.Result, error) { + ctx, span := tracer.Start(ctx, "policysummary.Controller.Process") + defer span.End() + + span.SetAttributes( + attribute.String("item.scope_type", item.ScopeType), + attribute.String("item.scope_id", item.ScopeID), + ) + + result, err := Reconcile(ctx, item.WorkspaceID, item.ScopeType, item.ScopeID, c.getter, c.setter) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return reconcile.Result{}, fmt.Errorf("reconcile policy summary: %w", err) + } + + if result.NextReconcileAt != nil { + span.SetAttributes(attribute.String("next_reconcile_at", result.NextReconcileAt.Format(time.RFC3339))) + return reconcile.Result{RequeueAfter: time.Until(*result.NextReconcileAt)}, nil + } + + return reconcile.Result{}, nil +} + +func NewController(getter Getter, setter Setter) *Controller { + return &Controller{getter: getter, setter: setter} +} + +func New(workerID string, pgxPool *pgxpool.Pool) svc.Service { + if pgxPool == nil { + log.Fatal("Failed to get pgx pool") + panic("failed to get pgx pool") + } + log.Debug( + "Creating policy summary reconcile worker", + "maxConcurrency", runtime.GOMAXPROCS(0), + ) + + nodeConfig := reconcile.NodeConfig{ + WorkerID: workerID, + BatchSize: 10, + PollInterval: 1 * time.Second, + LeaseDuration: 10 * time.Second, + LeaseHeartbeat: 5 * time.Second, + MaxConcurrency: runtime.GOMAXPROCS(0), + MaxRetryBackoff: 10 * time.Second, + } + + kind := events.PolicySummaryKind + queue := postgres.NewForKinds(pgxPool, kind) + controller := &Controller{ + getter: &PostgresGetter{}, + setter: &PostgresSetter{}, + } + worker, err := reconcile.NewWorker( + kind, + queue, + controller, + nodeConfig, + ) + if err != nil { + log.Fatal("Failed to create policy summary reconcile worker", "error", err) + } + + return worker +} diff --git a/apps/workspace-engine/svc/controllers/policysummary/getters.go b/apps/workspace-engine/svc/controllers/policysummary/getters.go new file mode 100644 index 000000000..a9df6d1f9 --- /dev/null +++ b/apps/workspace-engine/svc/controllers/policysummary/getters.go @@ -0,0 +1,22 @@ +package policysummary + +import ( + "context" + + "workspace-engine/pkg/oapi" + + "github.com/google/uuid" +) + +type Getter interface { + GetEnvironment(ctx context.Context, environmentID uuid.UUID) (*oapi.Environment, error) + GetDeployment(ctx context.Context, deploymentID uuid.UUID) (*oapi.Deployment, error) + GetVersion(ctx context.Context, versionID uuid.UUID) (*oapi.DeploymentVersion, error) + + GetPoliciesForEnvironment(ctx context.Context, workspaceID, environmentID uuid.UUID) ([]*oapi.Policy, error) + GetPoliciesForDeployment(ctx context.Context, workspaceID, deploymentID uuid.UUID) ([]*oapi.Policy, error) + + // TODO: These may need to be added or adapted from existing store methods. + // The summary evaluators use getters internally; these are for building the + // EvaluatorScope that gets passed to the evaluators. +} diff --git a/apps/workspace-engine/svc/controllers/policysummary/getters_postgres.go b/apps/workspace-engine/svc/controllers/policysummary/getters_postgres.go new file mode 100644 index 000000000..ecb28be1f --- /dev/null +++ b/apps/workspace-engine/svc/controllers/policysummary/getters_postgres.go @@ -0,0 +1,39 @@ +package policysummary + +import ( + "context" + "fmt" + + "workspace-engine/pkg/oapi" + + "github.com/google/uuid" +) + +type PostgresGetter struct{} + +var _ Getter = (*PostgresGetter)(nil) + +func (g *PostgresGetter) GetEnvironment(ctx context.Context, environmentID uuid.UUID) (*oapi.Environment, error) { + // TODO: query environment by ID from postgres + return nil, fmt.Errorf("not implemented") +} + +func (g *PostgresGetter) GetDeployment(ctx context.Context, deploymentID uuid.UUID) (*oapi.Deployment, error) { + // TODO: query deployment by ID from postgres + return nil, fmt.Errorf("not implemented") +} + +func (g *PostgresGetter) GetVersion(ctx context.Context, versionID uuid.UUID) (*oapi.DeploymentVersion, error) { + // TODO: query deployment_version by ID from postgres + return nil, fmt.Errorf("not implemented") +} + +func (g *PostgresGetter) GetPoliciesForEnvironment(ctx context.Context, workspaceID, environmentID uuid.UUID) ([]*oapi.Policy, error) { + // TODO: query policies whose selector matches this environment + return nil, fmt.Errorf("not implemented") +} + +func (g *PostgresGetter) GetPoliciesForDeployment(ctx context.Context, workspaceID, deploymentID uuid.UUID) ([]*oapi.Policy, error) { + // TODO: query policies whose selector matches this deployment + return nil, fmt.Errorf("not implemented") +} diff --git a/apps/workspace-engine/svc/controllers/policysummary/reconcile.go b/apps/workspace-engine/svc/controllers/policysummary/reconcile.go new file mode 100644 index 000000000..bf9843d47 --- /dev/null +++ b/apps/workspace-engine/svc/controllers/policysummary/reconcile.go @@ -0,0 +1,224 @@ +package policysummary + +import ( + "context" + "fmt" + "time" + + "workspace-engine/pkg/oapi" + "workspace-engine/pkg/reconcile/events" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator" + + "github.com/google/uuid" +) + +type ReconcileResult struct { + NextReconcileAt *time.Time +} + +type reconciler struct { + workspaceID uuid.UUID + getter Getter + setter Setter +} + +// reconcileEnvironment handles the "environment" scope channel. +// Runs: deployment window evaluator. +func (r *reconciler) reconcileEnvironment(ctx context.Context, scope *EnvironmentScope) (*ReconcileResult, error) { + ctx, span := tracer.Start(ctx, "policysummary.reconcileEnvironment") + defer span.End() + + env, err := r.getter.GetEnvironment(ctx, scope.EnvironmentID) + if err != nil { + return nil, fmt.Errorf("get environment: %w", err) + } + + evalScope := evaluator.EvaluatorScope{ + Environment: env, + } + + policies, err := r.getter.GetPoliciesForEnvironment(ctx, r.workspaceID, scope.EnvironmentID) + if err != nil { + return nil, fmt.Errorf("get policies: %w", err) + } + + rows, nextTime := r.evaluateAndCollect(ctx, policies, evalScope, func(rule *oapi.PolicyRule) []evaluator.Evaluator { + // TODO: return only environment-scoped summary evaluators (deployment window) + return nil + }) + + for i := range rows { + rows[i].EnvironmentID = &scope.EnvironmentID + } + + if err := r.setter.UpsertRuleSummaries(ctx, rows); err != nil { + return nil, fmt.Errorf("upsert rule summaries: %w", err) + } + + return &ReconcileResult{NextReconcileAt: nextTime}, nil +} + +// reconcileEnvironmentVersion handles the "environment-version" scope channel. +// Runs: approval, environment progression, gradual rollout evaluators. +func (r *reconciler) reconcileEnvironmentVersion(ctx context.Context, scope *EnvironmentVersionScope) (*ReconcileResult, error) { + ctx, span := tracer.Start(ctx, "policysummary.reconcileEnvironmentVersion") + defer span.End() + + env, err := r.getter.GetEnvironment(ctx, scope.EnvironmentID) + if err != nil { + return nil, fmt.Errorf("get environment: %w", err) + } + + version, err := r.getter.GetVersion(ctx, scope.VersionID) + if err != nil { + return nil, fmt.Errorf("get version: %w", err) + } + + evalScope := evaluator.EvaluatorScope{ + Environment: env, + Version: version, + } + + policies, err := r.getter.GetPoliciesForEnvironment(ctx, r.workspaceID, scope.EnvironmentID) + if err != nil { + return nil, fmt.Errorf("get policies: %w", err) + } + + rows, nextTime := r.evaluateAndCollect(ctx, policies, evalScope, func(rule *oapi.PolicyRule) []evaluator.Evaluator { + // TODO: return environment-version-scoped summary evaluators + // (approval, environment progression, gradual rollout) + return nil + }) + + for i := range rows { + rows[i].EnvironmentID = &scope.EnvironmentID + rows[i].VersionID = &scope.VersionID + } + + if err := r.setter.UpsertRuleSummaries(ctx, rows); err != nil { + return nil, fmt.Errorf("upsert rule summaries: %w", err) + } + + return &ReconcileResult{NextReconcileAt: nextTime}, nil +} + +// reconcileDeploymentVersion handles the "deployment-version" scope channel. +// Runs: version cooldown evaluator. +func (r *reconciler) reconcileDeploymentVersion(ctx context.Context, scope *DeploymentVersionScope) (*ReconcileResult, error) { + ctx, span := tracer.Start(ctx, "policysummary.reconcileDeploymentVersion") + defer span.End() + + deployment, err := r.getter.GetDeployment(ctx, scope.DeploymentID) + if err != nil { + return nil, fmt.Errorf("get deployment: %w", err) + } + + version, err := r.getter.GetVersion(ctx, scope.VersionID) + if err != nil { + return nil, fmt.Errorf("get version: %w", err) + } + + evalScope := evaluator.EvaluatorScope{ + Deployment: deployment, + Version: version, + } + + policies, err := r.getter.GetPoliciesForDeployment(ctx, r.workspaceID, scope.DeploymentID) + if err != nil { + return nil, fmt.Errorf("get policies: %w", err) + } + + rows, nextTime := r.evaluateAndCollect(ctx, policies, evalScope, func(rule *oapi.PolicyRule) []evaluator.Evaluator { + // TODO: return deployment-version-scoped summary evaluators (version cooldown) + return nil + }) + + for i := range rows { + rows[i].DeploymentID = &scope.DeploymentID + rows[i].VersionID = &scope.VersionID + } + + if err := r.setter.UpsertRuleSummaries(ctx, rows); err != nil { + return nil, fmt.Errorf("upsert rule summaries: %w", err) + } + + return &ReconcileResult{NextReconcileAt: nextTime}, nil +} + +// evaluateAndCollect runs the given evaluator factory against all policies and +// collects RuleSummaryRows. Returns the rows and the earliest NextEvaluationTime +// across all evaluations (for RequeueAfter). +func (r *reconciler) evaluateAndCollect( + ctx context.Context, + policies []*oapi.Policy, + scope evaluator.EvaluatorScope, + evaluatorFactory func(rule *oapi.PolicyRule) []evaluator.Evaluator, +) ([]RuleSummaryRow, *time.Time) { + var rows []RuleSummaryRow + var nextTime *time.Time + + for _, p := range policies { + for _, rule := range p.Rules { + evals := evaluatorFactory(&rule) + for _, eval := range evals { + if eval == nil { + continue + } + if !scope.HasFields(eval.ScopeFields()) { + continue + } + + result := eval.Evaluate(ctx, scope) + rows = append(rows, RuleSummaryRow{ + WorkspaceID: r.workspaceID, + PolicyID: uuid.MustParse(p.Id), + RuleID: rule.Id, + RuleType: eval.RuleType(), + Evaluation: result, + }) + + if result.NextEvaluationTime != nil { + if nextTime == nil || result.NextEvaluationTime.Before(*nextTime) { + nextTime = result.NextEvaluationTime + } + } + } + } + } + + return rows, nextTime +} + +func Reconcile(ctx context.Context, workspaceID string, scopeType string, scopeID string, getter Getter, setter Setter) (*ReconcileResult, error) { + r := &reconciler{ + workspaceID: uuid.MustParse(workspaceID), + getter: getter, + setter: setter, + } + + switch scopeType { + case events.PolicySummaryScopeEnvironment: + scope, err := ParseEnvironmentScope(scopeID) + if err != nil { + return nil, err + } + return r.reconcileEnvironment(ctx, scope) + + case events.PolicySummaryScopeEnvironmentVersion: + scope, err := ParseEnvironmentVersionScope(scopeID) + if err != nil { + return nil, err + } + return r.reconcileEnvironmentVersion(ctx, scope) + + case events.PolicySummaryScopeDeploymentVersion: + scope, err := ParseDeploymentVersionScope(scopeID) + if err != nil { + return nil, err + } + return r.reconcileDeploymentVersion(ctx, scope) + + default: + return nil, fmt.Errorf("unknown policy summary scope type: %s", scopeType) + } +} diff --git a/apps/workspace-engine/svc/controllers/policysummary/scope.go b/apps/workspace-engine/svc/controllers/policysummary/scope.go new file mode 100644 index 000000000..92e08d0e7 --- /dev/null +++ b/apps/workspace-engine/svc/controllers/policysummary/scope.go @@ -0,0 +1,62 @@ +package policysummary + +import ( + "fmt" + "strings" + + "github.com/google/uuid" +) + +type EnvironmentScope struct { + EnvironmentID uuid.UUID +} + +func ParseEnvironmentScope(scopeID string) (*EnvironmentScope, error) { + envID, err := uuid.Parse(scopeID) + if err != nil { + return nil, fmt.Errorf("parse environment scope: %w", err) + } + return &EnvironmentScope{EnvironmentID: envID}, nil +} + +type EnvironmentVersionScope struct { + EnvironmentID uuid.UUID + VersionID uuid.UUID +} + +func ParseEnvironmentVersionScope(scopeID string) (*EnvironmentVersionScope, error) { + parts := strings.SplitN(scopeID, ":", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid environment-version scope: %s", scopeID) + } + envID, err := uuid.Parse(parts[0]) + if err != nil { + return nil, fmt.Errorf("parse environment id: %w", err) + } + versionID, err := uuid.Parse(parts[1]) + if err != nil { + return nil, fmt.Errorf("parse version id: %w", err) + } + return &EnvironmentVersionScope{EnvironmentID: envID, VersionID: versionID}, nil +} + +type DeploymentVersionScope struct { + DeploymentID uuid.UUID + VersionID uuid.UUID +} + +func ParseDeploymentVersionScope(scopeID string) (*DeploymentVersionScope, error) { + parts := strings.SplitN(scopeID, ":", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid deployment-version scope: %s", scopeID) + } + depID, err := uuid.Parse(parts[0]) + if err != nil { + return nil, fmt.Errorf("parse deployment id: %w", err) + } + versionID, err := uuid.Parse(parts[1]) + if err != nil { + return nil, fmt.Errorf("parse version id: %w", err) + } + return &DeploymentVersionScope{DeploymentID: depID, VersionID: versionID}, nil +} diff --git a/apps/workspace-engine/svc/controllers/policysummary/setters.go b/apps/workspace-engine/svc/controllers/policysummary/setters.go new file mode 100644 index 000000000..d17d4ca86 --- /dev/null +++ b/apps/workspace-engine/svc/controllers/policysummary/setters.go @@ -0,0 +1,25 @@ +package policysummary + +import ( + "context" + + "workspace-engine/pkg/oapi" + + "github.com/google/uuid" +) + +// RuleSummaryRow is the DB representation of a single rule evaluation result. +type RuleSummaryRow struct { + WorkspaceID uuid.UUID + PolicyID uuid.UUID + RuleID string + RuleType string + DeploymentID *uuid.UUID + EnvironmentID *uuid.UUID + VersionID *uuid.UUID + Evaluation *oapi.RuleEvaluation +} + +type Setter interface { + UpsertRuleSummaries(ctx context.Context, rows []RuleSummaryRow) error +} diff --git a/apps/workspace-engine/svc/controllers/policysummary/setters_postgres.go b/apps/workspace-engine/svc/controllers/policysummary/setters_postgres.go new file mode 100644 index 000000000..ab1907395 --- /dev/null +++ b/apps/workspace-engine/svc/controllers/policysummary/setters_postgres.go @@ -0,0 +1,20 @@ +package policysummary + +import ( + "context" + "fmt" +) + +type PostgresSetter struct{} + +var _ Setter = (*PostgresSetter)(nil) + +func (s *PostgresSetter) UpsertRuleSummaries(ctx context.Context, rows []RuleSummaryRow) error { + // TODO: batch upsert into policy_rule_summary table + // ON CONFLICT (rule_id, deployment_id, environment_id, version_id) DO UPDATE + // SET allowed = EXCLUDED.allowed, message = EXCLUDED.message, ... + if len(rows) == 0 { + return nil + } + return fmt.Errorf("not implemented") +} diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts index 383c58937..8d99f3c93 100644 --- a/packages/db/src/schema/index.ts +++ b/packages/db/src/schema/index.ts @@ -22,5 +22,6 @@ export * from "./resource-variable.js"; export * from "./deployment-variable.js"; export * from "./workflow.js"; export * from "./policy-skip.js"; +export * from "./policy-rule-summary.js"; export * from "./job-verification-metric.js"; export * from "./relationships.js"; diff --git a/packages/db/src/schema/policy-rule-summary.ts b/packages/db/src/schema/policy-rule-summary.ts new file mode 100644 index 000000000..347308b48 --- /dev/null +++ b/packages/db/src/schema/policy-rule-summary.ts @@ -0,0 +1,82 @@ +import { relations } from "drizzle-orm"; +import { + boolean, + index, + jsonb, + pgTable, + text, + timestamp, + uniqueIndex, + uuid, +} from "drizzle-orm/pg-core"; + +import { deployment } from "./deployment.js"; +import { deploymentVersion } from "./deployment-version.js"; +import { environment } from "./environment.js"; +import { policy } from "./policy.js"; +import { workspace } from "./workspace.js"; + +export const policyRuleSummary = pgTable( + "policy_rule_summary", + { + id: uuid("id").primaryKey().defaultRandom(), + workspaceId: uuid("workspace_id") + .notNull() + .references(() => workspace.id, { onDelete: "cascade" }), + policyId: uuid("policy_id") + .notNull() + .references(() => policy.id, { onDelete: "cascade" }), + ruleId: text("rule_id").notNull(), + ruleType: text("rule_type").notNull(), + + deploymentId: uuid("deployment_id").references(() => deployment.id, { + onDelete: "cascade", + }), + environmentId: uuid("environment_id").references(() => environment.id, { + onDelete: "cascade", + }), + versionId: uuid("version_id").references(() => deploymentVersion.id, { + onDelete: "cascade", + }), + + allowed: boolean("allowed").notNull(), + actionRequired: boolean("action_required").notNull().default(false), + actionType: text("action_type"), + message: text("message").notNull(), + details: jsonb("details").notNull().default("{}"), + + satisfiedAt: timestamp("satisfied_at", { withTimezone: true }), + nextEvaluationAt: timestamp("next_evaluation_at", { withTimezone: true }), + evaluatedAt: timestamp("evaluated_at", { withTimezone: true }) + .notNull() + .defaultNow(), + }, + (t) => [ + uniqueIndex().on(t.ruleId, t.deploymentId, t.environmentId, t.versionId), + index().on(t.deploymentId, t.versionId), + index().on(t.environmentId), + index().on(t.workspaceId), + ], +); + +export const policyRuleSummaryRelations = relations( + policyRuleSummary, + ({ one }) => ({ + policy: one(policy, { + fields: [policyRuleSummary.policyId], + references: [policy.id], + }), + deployment: one(deployment, { + fields: [policyRuleSummary.deploymentId], + references: [deployment.id], + }), + environment: one(environment, { + fields: [policyRuleSummary.environmentId], + references: [environment.id], + }), + version: one(deploymentVersion, { + fields: [policyRuleSummary.versionId], + references: [deploymentVersion.id], + }), + }), +); From 83e313645aa30ebbcb71a926981287e463aa7de4 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Fri, 6 Mar 2026 17:28:28 -0800 Subject: [PATCH 02/11] more --- .../svc/controllers/policysummary/reconcile.go | 4 +--- .../svc/controllers/policysummary/setters.go | 4 +--- packages/db/src/schema/policy-rule-summary.ts | 11 +---------- 3 files changed, 3 insertions(+), 16 deletions(-) diff --git a/apps/workspace-engine/svc/controllers/policysummary/reconcile.go b/apps/workspace-engine/svc/controllers/policysummary/reconcile.go index bf9843d47..6fe42e3fb 100644 --- a/apps/workspace-engine/svc/controllers/policysummary/reconcile.go +++ b/apps/workspace-engine/svc/controllers/policysummary/reconcile.go @@ -171,9 +171,7 @@ func (r *reconciler) evaluateAndCollect( result := eval.Evaluate(ctx, scope) rows = append(rows, RuleSummaryRow{ WorkspaceID: r.workspaceID, - PolicyID: uuid.MustParse(p.Id), - RuleID: rule.Id, - RuleType: eval.RuleType(), + RuleID: uuid.MustParse(rule.Id), Evaluation: result, }) diff --git a/apps/workspace-engine/svc/controllers/policysummary/setters.go b/apps/workspace-engine/svc/controllers/policysummary/setters.go index d17d4ca86..3f4771ca6 100644 --- a/apps/workspace-engine/svc/controllers/policysummary/setters.go +++ b/apps/workspace-engine/svc/controllers/policysummary/setters.go @@ -11,9 +11,7 @@ import ( // RuleSummaryRow is the DB representation of a single rule evaluation result. type RuleSummaryRow struct { WorkspaceID uuid.UUID - PolicyID uuid.UUID - RuleID string - RuleType string + RuleID uuid.UUID DeploymentID *uuid.UUID EnvironmentID *uuid.UUID VersionID *uuid.UUID diff --git a/packages/db/src/schema/policy-rule-summary.ts b/packages/db/src/schema/policy-rule-summary.ts index 347308b48..59077ff20 100644 --- a/packages/db/src/schema/policy-rule-summary.ts +++ b/packages/db/src/schema/policy-rule-summary.ts @@ -13,7 +13,6 @@ import { import { deployment } from "./deployment.js"; import { deploymentVersion } from "./deployment-version.js"; import { environment } from "./environment.js"; -import { policy } from "./policy.js"; import { workspace } from "./workspace.js"; export const policyRuleSummary = pgTable( @@ -23,11 +22,7 @@ export const policyRuleSummary = pgTable( workspaceId: uuid("workspace_id") .notNull() .references(() => workspace.id, { onDelete: "cascade" }), - policyId: uuid("policy_id") - .notNull() - .references(() => policy.id, { onDelete: "cascade" }), - ruleId: text("rule_id").notNull(), - ruleType: text("rule_type").notNull(), + ruleId: uuid("rule_id").notNull(), deploymentId: uuid("deployment_id").references(() => deployment.id, { onDelete: "cascade", @@ -62,10 +57,6 @@ export const policyRuleSummary = pgTable( export const policyRuleSummaryRelations = relations( policyRuleSummary, ({ one }) => ({ - policy: one(policy, { - fields: [policyRuleSummary.policyId], - references: [policy.id], - }), deployment: one(deployment, { fields: [policyRuleSummary.deploymentId], references: [deployment.id], From 87266c5b5d77053ce6899e84f6aea5b5076a198e Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Fri, 6 Mar 2026 19:17:41 -0800 Subject: [PATCH 03/11] finish --- apps/workspace-engine/pkg/db/batch.go | 89 + apps/workspace-engine/pkg/db/models.go | 16 + .../pkg/db/policy_rule_summary.sql.go | 163 + .../pkg/db/queries/policy_rule_summary.sql | 54 + .../pkg/db/queries/schema.sql | 19 + apps/workspace-engine/pkg/db/sqlc.yaml | 6 + .../deploymentversion/deploymentversion.go | 35 + .../events/handler/environment/environment.go | 14 + .../pkg/events/handler/jobs/jobs.go | 23 +- .../pkg/events/handler/policies/policies.go | 30 +- .../userapprovalrecords.go | 17 + .../releasemanager/policy/evaluators.go | 28 - .../controller.go | 22 + .../controllers/policysummary/controller.go | 6 +- .../svc/controllers/policysummary/getters.go | 12 +- .../policysummary/getters_postgres.go | 54 +- .../policysummary/getters_postgres_test.go | 235 + .../controllers/policysummary/reconcile.go | 31 +- .../svc/controllers/policysummary/setters.go | 1 - .../policysummary/setters_postgres.go | 74 +- .../policysummary/summaryeval/getter.go | 20 + .../summaryeval/getter_postgres.go | 39 + .../policysummary/summaryeval/summaryeval.go | 29 + .../summaryeval/summaryeval_test.go | 403 ++ packages/db/drizzle/0162_same_kronos.sql | 22 + packages/db/drizzle/meta/0162_snapshot.json | 5843 +++++++++++++++++ packages/db/drizzle/meta/_journal.json | 9 +- packages/db/src/schema/policy-rule-summary.ts | 5 - 28 files changed, 7211 insertions(+), 88 deletions(-) create mode 100644 apps/workspace-engine/pkg/db/policy_rule_summary.sql.go create mode 100644 apps/workspace-engine/pkg/db/queries/policy_rule_summary.sql create mode 100644 apps/workspace-engine/svc/controllers/policysummary/getters_postgres_test.go create mode 100644 apps/workspace-engine/svc/controllers/policysummary/summaryeval/getter.go create mode 100644 apps/workspace-engine/svc/controllers/policysummary/summaryeval/getter_postgres.go create mode 100644 apps/workspace-engine/svc/controllers/policysummary/summaryeval/summaryeval.go create mode 100644 apps/workspace-engine/svc/controllers/policysummary/summaryeval/summaryeval_test.go create mode 100644 packages/db/drizzle/0162_same_kronos.sql create mode 100644 packages/db/drizzle/meta/0162_snapshot.json diff --git a/apps/workspace-engine/pkg/db/batch.go b/apps/workspace-engine/pkg/db/batch.go index c44acb790..7817792d6 100644 --- a/apps/workspace-engine/pkg/db/batch.go +++ b/apps/workspace-engine/pkg/db/batch.go @@ -205,3 +205,92 @@ func (b *UpsertChangelogEntryBatchResults) Close() error { b.closed = true return b.br.Close() } + +const upsertPolicyRuleSummary = `-- name: UpsertPolicyRuleSummary :batchexec +INSERT INTO policy_rule_summary ( + id, rule_id, + deployment_id, environment_id, version_id, + allowed, action_required, action_type, + message, details, + satisfied_at, next_evaluation_at, evaluated_at +) +VALUES ( + gen_random_uuid(), $1, + $2, $3, $4, + $5, $6, $7, + $8, $9, + $10, $11, NOW() +) +ON CONFLICT (rule_id, deployment_id, environment_id, version_id) DO UPDATE +SET allowed = EXCLUDED.allowed, + action_required = EXCLUDED.action_required, + action_type = EXCLUDED.action_type, + message = EXCLUDED.message, + details = EXCLUDED.details, + satisfied_at = EXCLUDED.satisfied_at, + next_evaluation_at = EXCLUDED.next_evaluation_at, + evaluated_at = NOW() +` + +type UpsertPolicyRuleSummaryBatchResults struct { + br pgx.BatchResults + tot int + closed bool +} + +type UpsertPolicyRuleSummaryParams struct { + RuleID uuid.UUID + DeploymentID uuid.UUID + EnvironmentID uuid.UUID + VersionID uuid.UUID + Allowed bool + ActionRequired bool + ActionType pgtype.Text + Message string + Details map[string]any + SatisfiedAt pgtype.Timestamptz + NextEvaluationAt pgtype.Timestamptz +} + +func (q *Queries) UpsertPolicyRuleSummary(ctx context.Context, arg []UpsertPolicyRuleSummaryParams) *UpsertPolicyRuleSummaryBatchResults { + batch := &pgx.Batch{} + for _, a := range arg { + vals := []interface{}{ + a.RuleID, + a.DeploymentID, + a.EnvironmentID, + a.VersionID, + a.Allowed, + a.ActionRequired, + a.ActionType, + a.Message, + a.Details, + a.SatisfiedAt, + a.NextEvaluationAt, + } + batch.Queue(upsertPolicyRuleSummary, vals...) + } + br := q.db.SendBatch(ctx, batch) + return &UpsertPolicyRuleSummaryBatchResults{br, len(arg), false} +} + +func (b *UpsertPolicyRuleSummaryBatchResults) Exec(f func(int, error)) { + defer b.br.Close() + for t := 0; t < b.tot; t++ { + if b.closed { + if f != nil { + f(t, ErrBatchAlreadyClosed) + } + continue + } + _, err := b.br.Exec() + if f != nil { + f(t, err) + } + } +} + +func (b *UpsertPolicyRuleSummaryBatchResults) Close() error { + b.closed = true + return b.br.Close() +} diff --git a/apps/workspace-engine/pkg/db/models.go b/apps/workspace-engine/pkg/db/models.go index 74db64892..d04d3b5a6 100644 --- a/apps/workspace-engine/pkg/db/models.go +++ b/apps/workspace-engine/pkg/db/models.go @@ -464,6 +464,22 @@ type PolicyRuleRollback struct { CreatedAt pgtype.Timestamptz } +type PolicyRuleSummary struct { + ID uuid.UUID + RuleID uuid.UUID + DeploymentID uuid.UUID + EnvironmentID uuid.UUID + VersionID uuid.UUID + Allowed bool + ActionRequired bool + ActionType pgtype.Text + Message string + Details map[string]any + SatisfiedAt pgtype.Timestamptz + NextEvaluationAt pgtype.Timestamptz + EvaluatedAt pgtype.Timestamptz +} + type PolicyRuleVerification struct { ID uuid.UUID PolicyID uuid.UUID diff --git a/apps/workspace-engine/pkg/db/policy_rule_summary.sql.go b/apps/workspace-engine/pkg/db/policy_rule_summary.sql.go new file mode 100644 index 000000000..d4efad227 --- /dev/null +++ b/apps/workspace-engine/pkg/db/policy_rule_summary.sql.go @@ -0,0 +1,163 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: policy_rule_summary.sql + +package db + +import ( + "context" + + "github.com/google/uuid" +) + +const deletePolicyRuleSummariesByRuleID = `-- name: DeletePolicyRuleSummariesByRuleID :exec +DELETE FROM policy_rule_summary WHERE rule_id = $1 +` + +func (q *Queries) DeletePolicyRuleSummariesByRuleID(ctx context.Context, ruleID uuid.UUID) error { + _, err := q.db.Exec(ctx, deletePolicyRuleSummariesByRuleID, ruleID) + return err +} + +const listPolicyRuleSummariesByDeploymentAndVersion = `-- name: ListPolicyRuleSummariesByDeploymentAndVersion :many +SELECT id, rule_id, + deployment_id, environment_id, version_id, + allowed, action_required, action_type, + message, details, + satisfied_at, next_evaluation_at, evaluated_at +FROM policy_rule_summary +WHERE deployment_id = $1 AND version_id = $2 +` + +type ListPolicyRuleSummariesByDeploymentAndVersionParams struct { + DeploymentID uuid.UUID + VersionID uuid.UUID +} + +func (q *Queries) ListPolicyRuleSummariesByDeploymentAndVersion(ctx context.Context, arg ListPolicyRuleSummariesByDeploymentAndVersionParams) ([]PolicyRuleSummary, error) { + rows, err := q.db.Query(ctx, listPolicyRuleSummariesByDeploymentAndVersion, arg.DeploymentID, arg.VersionID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PolicyRuleSummary + for rows.Next() { + var i PolicyRuleSummary + if err := rows.Scan( + &i.ID, + &i.RuleID, + &i.DeploymentID, + &i.EnvironmentID, + &i.VersionID, + &i.Allowed, + &i.ActionRequired, + &i.ActionType, + &i.Message, + &i.Details, + &i.SatisfiedAt, + &i.NextEvaluationAt, + &i.EvaluatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listPolicyRuleSummariesByEnvironment = `-- name: ListPolicyRuleSummariesByEnvironment :many +SELECT id, rule_id, + deployment_id, environment_id, version_id, + allowed, action_required, action_type, + message, details, + satisfied_at, next_evaluation_at, evaluated_at +FROM policy_rule_summary +WHERE environment_id = $1 +` + +func (q *Queries) ListPolicyRuleSummariesByEnvironment(ctx context.Context, environmentID uuid.UUID) ([]PolicyRuleSummary, error) { + rows, err := q.db.Query(ctx, listPolicyRuleSummariesByEnvironment, environmentID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PolicyRuleSummary + for rows.Next() { + var i PolicyRuleSummary + if err := rows.Scan( + &i.ID, + &i.RuleID, + &i.DeploymentID, + &i.EnvironmentID, + &i.VersionID, + &i.Allowed, + &i.ActionRequired, + &i.ActionType, + &i.Message, + &i.Details, + &i.SatisfiedAt, + &i.NextEvaluationAt, + &i.EvaluatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listPolicyRuleSummariesByEnvironmentAndVersion = `-- name: ListPolicyRuleSummariesByEnvironmentAndVersion :many +SELECT id, rule_id, + deployment_id, environment_id, version_id, + allowed, action_required, action_type, + message, details, + satisfied_at, next_evaluation_at, evaluated_at +FROM policy_rule_summary +WHERE environment_id = $1 AND version_id = $2 +` + +type ListPolicyRuleSummariesByEnvironmentAndVersionParams struct { + EnvironmentID uuid.UUID + VersionID uuid.UUID +} + +func (q *Queries) ListPolicyRuleSummariesByEnvironmentAndVersion(ctx context.Context, arg ListPolicyRuleSummariesByEnvironmentAndVersionParams) ([]PolicyRuleSummary, error) { + rows, err := q.db.Query(ctx, listPolicyRuleSummariesByEnvironmentAndVersion, arg.EnvironmentID, arg.VersionID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PolicyRuleSummary + for rows.Next() { + var i PolicyRuleSummary + if err := rows.Scan( + &i.ID, + &i.RuleID, + &i.DeploymentID, + &i.EnvironmentID, + &i.VersionID, + &i.Allowed, + &i.ActionRequired, + &i.ActionType, + &i.Message, + &i.Details, + &i.SatisfiedAt, + &i.NextEvaluationAt, + &i.EvaluatedAt, + ); 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/queries/policy_rule_summary.sql b/apps/workspace-engine/pkg/db/queries/policy_rule_summary.sql new file mode 100644 index 000000000..2829d29b2 --- /dev/null +++ b/apps/workspace-engine/pkg/db/queries/policy_rule_summary.sql @@ -0,0 +1,54 @@ +-- name: UpsertPolicyRuleSummary :batchexec +INSERT INTO policy_rule_summary ( + id, rule_id, + deployment_id, environment_id, version_id, + allowed, action_required, action_type, + message, details, + satisfied_at, next_evaluation_at, evaluated_at +) +VALUES ( + gen_random_uuid(), $1, + $2, $3, $4, + $5, $6, $7, + $8, $9, + $10, $11, NOW() +) +ON CONFLICT (rule_id, deployment_id, environment_id, version_id) DO UPDATE +SET allowed = EXCLUDED.allowed, + action_required = EXCLUDED.action_required, + action_type = EXCLUDED.action_type, + message = EXCLUDED.message, + details = EXCLUDED.details, + satisfied_at = EXCLUDED.satisfied_at, + next_evaluation_at = EXCLUDED.next_evaluation_at, + evaluated_at = NOW(); + +-- name: ListPolicyRuleSummariesByDeploymentAndVersion :many +SELECT id, rule_id, + deployment_id, environment_id, version_id, + allowed, action_required, action_type, + message, details, + satisfied_at, next_evaluation_at, evaluated_at +FROM policy_rule_summary +WHERE deployment_id = $1 AND version_id = $2; + +-- name: ListPolicyRuleSummariesByEnvironment :many +SELECT id, rule_id, + deployment_id, environment_id, version_id, + allowed, action_required, action_type, + message, details, + satisfied_at, next_evaluation_at, evaluated_at +FROM policy_rule_summary +WHERE environment_id = $1; + +-- name: ListPolicyRuleSummariesByEnvironmentAndVersion :many +SELECT id, rule_id, + deployment_id, environment_id, version_id, + allowed, action_required, action_type, + message, details, + satisfied_at, next_evaluation_at, evaluated_at +FROM policy_rule_summary +WHERE environment_id = $1 AND version_id = $2; + +-- name: DeletePolicyRuleSummariesByRuleID :exec +DELETE FROM policy_rule_summary WHERE rule_id = $1; diff --git a/apps/workspace-engine/pkg/db/queries/schema.sql b/apps/workspace-engine/pkg/db/queries/schema.sql index a9884fd67..2e49ccb1b 100644 --- a/apps/workspace-engine/pkg/db/queries/schema.sql +++ b/apps/workspace-engine/pkg/db/queries/schema.sql @@ -425,3 +425,22 @@ CREATE TABLE job_verification_metric_measurement ( message TEXT NOT NULL DEFAULT '', status job_verification_status NOT NULL ); + +CREATE TABLE policy_rule_summary ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + rule_id UUID NOT NULL, + deployment_id UUID, + environment_id UUID, + version_id UUID, + allowed BOOLEAN NOT NULL, + action_required BOOLEAN NOT NULL DEFAULT false, + action_type TEXT, + message TEXT NOT NULL, + details JSONB NOT NULL DEFAULT '{}'::jsonb, + satisfied_at TIMESTAMPTZ, + next_evaluation_at TIMESTAMPTZ, + evaluated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE UNIQUE INDEX policy_rule_summary_scope_idx + ON policy_rule_summary (rule_id, deployment_id, environment_id, version_id); diff --git a/apps/workspace-engine/pkg/db/sqlc.yaml b/apps/workspace-engine/pkg/db/sqlc.yaml index 2fdbca942..a81dd32a7 100644 --- a/apps/workspace-engine/pkg/db/sqlc.yaml +++ b/apps/workspace-engine/pkg/db/sqlc.yaml @@ -29,6 +29,7 @@ sql: - queries/jobs.sql - queries/computed_relationships.sql - queries/relationship_rules.sql + - queries/policy_rule_summary.sql database: uri: "postgresql://ctrlplane:ctrlplane@127.0.0.1:5432/ctrlplane?sslmode=disable" gen: @@ -124,6 +125,11 @@ sql: go_type: type: "map[string]string" + # PolicyRuleSummary + - column: "policy_rule_summary.details" + go_type: + type: "map[string]any" + # ResourceVariable - column: "resource_variable.value" go_type: diff --git a/apps/workspace-engine/pkg/events/handler/deploymentversion/deploymentversion.go b/apps/workspace-engine/pkg/events/handler/deploymentversion/deploymentversion.go index 06a7e70c0..b0576f356 100644 --- a/apps/workspace-engine/pkg/events/handler/deploymentversion/deploymentversion.go +++ b/apps/workspace-engine/pkg/events/handler/deploymentversion/deploymentversion.go @@ -6,16 +6,45 @@ import ( "workspace-engine/pkg/events/handler" "workspace-engine/pkg/oapi" + "workspace-engine/pkg/reconcile/events" "workspace-engine/pkg/workspace" "workspace-engine/pkg/workspace/releasemanager" "workspace-engine/pkg/workspace/releasemanager/trace" + "github.com/charmbracelet/log" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" ) var tracer = otel.Tracer("events/handler/deploymentversion") +func enqueuePolicySummaries(ctx context.Context, ws *workspace.Workspace, version *oapi.DeploymentVersion, releaseTargets []*oapi.ReleaseTarget) { + seen := make(map[string]struct{}) + var params []events.PolicySummaryParams + + for _, rt := range releaseTargets { + evKey := rt.EnvironmentId + ":" + version.Id + if _, ok := seen[evKey]; !ok { + seen[evKey] = struct{}{} + params = append(params, events.EnvironmentVersionSummaryParams{ + WorkspaceID: ws.ID, + EnvironmentID: rt.EnvironmentId, + VersionID: version.Id, + }.ToParams()) + } + } + + params = append(params, events.DeploymentVersionSummaryParams{ + WorkspaceID: ws.ID, + DeploymentID: version.DeploymentId, + VersionID: version.Id, + }.ToParams()) + + if err := events.EnqueueManyPolicySummary(ws.Queue(), ctx, params); err != nil { + log.Error("failed to enqueue policy summaries for version change", "error", err) + } +} + func HandleDeploymentVersionCreated( ctx context.Context, ws *workspace.Workspace, @@ -47,6 +76,8 @@ func HandleDeploymentVersionCreated( _ = ws.ReleaseManager().ReconcileTargets(ctx, releaseTargets, releasemanager.WithTrigger(trace.TriggerVersionCreated)) + enqueuePolicySummaries(ctx, ws, deploymentVersion, releaseTargets) + return nil } @@ -80,6 +111,8 @@ func HandleDeploymentVersionUpdated( _ = ws.ReleaseManager().ReconcileTargets(ctx, releaseTargets, releasemanager.WithTrigger(trace.TriggerVersionCreated)) + enqueuePolicySummaries(ctx, ws, deploymentVersion, releaseTargets) + return nil } @@ -114,5 +147,7 @@ func HandleDeploymentVersionDeleted( _ = ws.ReleaseManager().ReconcileTargets(ctx, releaseTargets, releasemanager.WithTrigger(trace.TriggerVersionCreated)) + enqueuePolicySummaries(ctx, ws, deploymentVersion, releaseTargets) + return nil } diff --git a/apps/workspace-engine/pkg/events/handler/environment/environment.go b/apps/workspace-engine/pkg/events/handler/environment/environment.go index 6d9f086c1..e1a3e6148 100644 --- a/apps/workspace-engine/pkg/events/handler/environment/environment.go +++ b/apps/workspace-engine/pkg/events/handler/environment/environment.go @@ -86,6 +86,13 @@ func HandleEnvironmentCreated( releasemanager.WithTrigger(trace.TriggerEnvironmentCreated)) } + if err := events.EnqueuePolicySummary(ws.Queue(), ctx, events.EnvironmentSummaryParams{ + WorkspaceID: ws.ID, + EnvironmentID: environment.Id, + }.ToParams()); err != nil { + log.Error("failed to enqueue policy summary for environment created", "error", err) + } + return nil } @@ -181,6 +188,13 @@ func HandleEnvironmentUpdated( _ = ws.ReleaseManager().ReconcileTargets(ctx, reconileReleaseTargets, releasemanager.WithTrigger(trace.TriggerEnvironmentUpdated)) + if err := events.EnqueuePolicySummary(ws.Queue(), ctx, events.EnvironmentSummaryParams{ + WorkspaceID: ws.ID, + EnvironmentID: environment.Id, + }.ToParams()); err != nil { + log.Error("failed to enqueue policy summary for environment updated", "error", err) + } + return nil } diff --git a/apps/workspace-engine/pkg/events/handler/jobs/jobs.go b/apps/workspace-engine/pkg/events/handler/jobs/jobs.go index 94cbeb15f..9849b1924 100644 --- a/apps/workspace-engine/pkg/events/handler/jobs/jobs.go +++ b/apps/workspace-engine/pkg/events/handler/jobs/jobs.go @@ -6,6 +6,7 @@ import ( "workspace-engine/pkg/events/handler" "workspace-engine/pkg/oapi" + "workspace-engine/pkg/reconcile/events" "workspace-engine/pkg/workspace" "github.com/charmbracelet/log" @@ -40,8 +41,10 @@ func HandleJobUpdated( if jobUpdateEvent.FieldsToUpdate == nil || len(*jobUpdateEvent.FieldsToUpdate) == 0 { ws.Jobs().Upsert(ctx, &jobUpdateEvent.Job) dirtyStateForJob(ctx, ws, &jobUpdateEvent.Job) - // Trigger actions on status change triggerActionsOnStatusChange(ctx, ws, &jobUpdateEvent.Job, previousStatus) + if jobUpdateEvent.Job.Status != previousStatus { + enqueuePolicySummaryForJob(ctx, ws, &jobUpdateEvent.Job) + } return nil } @@ -60,6 +63,10 @@ func HandleJobUpdated( // Trigger actions on status change triggerActionsOnStatusChange(ctx, ws, mergedJob, previousStatus) + if mergedJob.Status != previousStatus { + enqueuePolicySummaryForJob(ctx, ws, mergedJob) + } + go func() { if err := MaybeAddCommitStatusFromJob(ws, mergedJob); err != nil { log.Error("error adding commit status", "error", err.Error()) @@ -108,6 +115,20 @@ func dirtyStateForJob(ctx context.Context, ws *workspace.Workspace, job *oapi.Jo ws.ReleaseManager().RecomputeState(ctx) } +func enqueuePolicySummaryForJob(ctx context.Context, ws *workspace.Workspace, job *oapi.Job) { + release, exists := ws.Releases().Get(job.ReleaseId) + if !exists { + return + } + if err := events.EnqueuePolicySummary(ws.Queue(), ctx, events.EnvironmentVersionSummaryParams{ + WorkspaceID: ws.ID, + EnvironmentID: release.ReleaseTarget.EnvironmentId, + VersionID: release.Version.Id, + }.ToParams()); err != nil { + log.Error("failed to enqueue policy summary for job update", "error", err) + } +} + func getJob(ws *workspace.Workspace, job *oapi.JobUpdateEvent) (*oapi.Job, bool) { if job.Id != nil && *job.Id != "" { if existing, exists := ws.Jobs().Get(*job.Id); exists { diff --git a/apps/workspace-engine/pkg/events/handler/policies/policies.go b/apps/workspace-engine/pkg/events/handler/policies/policies.go index db98655be..696cd7f3b 100644 --- a/apps/workspace-engine/pkg/events/handler/policies/policies.go +++ b/apps/workspace-engine/pkg/events/handler/policies/policies.go @@ -2,14 +2,15 @@ package policies import ( "context" + "encoding/json" + "workspace-engine/pkg/events/handler" "workspace-engine/pkg/oapi" + "workspace-engine/pkg/reconcile/events" "workspace-engine/pkg/workspace" "workspace-engine/pkg/workspace/releasemanager" "workspace-engine/pkg/workspace/releasemanager/trace" - "encoding/json" - "github.com/charmbracelet/log" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" @@ -45,6 +46,25 @@ func getAffectedTargets(ctx context.Context, ws *workspace.Workspace, policyID s return affectedTargets } +func enqueuePolicySummariesForTargets(ctx context.Context, ws *workspace.Workspace, targets []*oapi.ReleaseTarget) { + envSeen := make(map[string]struct{}) + var params []events.PolicySummaryParams + + for _, rt := range targets { + if _, ok := envSeen[rt.EnvironmentId]; !ok { + envSeen[rt.EnvironmentId] = struct{}{} + params = append(params, events.EnvironmentSummaryParams{ + WorkspaceID: ws.ID, + EnvironmentID: rt.EnvironmentId, + }.ToParams()) + } + } + + if err := events.EnqueueManyPolicySummary(ws.Queue(), ctx, params); err != nil { + log.Error("failed to enqueue policy summaries for policy change", "error", err) + } +} + func HandlePolicyCreated( ctx context.Context, ws *workspace.Workspace, @@ -70,6 +90,8 @@ func HandlePolicyCreated( _ = ws.ReleaseManager().ReconcileTargets(ctx, affectedTargets, releasemanager.WithTrigger(trace.TriggerPolicyUpdated)) + enqueuePolicySummariesForTargets(ctx, ws, affectedTargets) + return nil } @@ -119,6 +141,8 @@ func HandlePolicyUpdated( _ = ws.ReleaseManager().ReconcileTargets(ctx, affectedTargets, releasemanager.WithTrigger(trace.TriggerPolicyUpdated)) + enqueuePolicySummariesForTargets(ctx, ws, affectedTargets) + return nil } @@ -144,5 +168,7 @@ func HandlePolicyDeleted( _ = ws.ReleaseManager().ReconcileTargets(ctx, affectedTargets, releasemanager.WithTrigger(trace.TriggerPolicyUpdated)) + enqueuePolicySummariesForTargets(ctx, ws, affectedTargets) + return nil } diff --git a/apps/workspace-engine/pkg/events/handler/userapprovalrecords/userapprovalrecords.go b/apps/workspace-engine/pkg/events/handler/userapprovalrecords/userapprovalrecords.go index 94be0a831..26b047075 100644 --- a/apps/workspace-engine/pkg/events/handler/userapprovalrecords/userapprovalrecords.go +++ b/apps/workspace-engine/pkg/events/handler/userapprovalrecords/userapprovalrecords.go @@ -7,6 +7,7 @@ import ( "workspace-engine/pkg/events/handler" "workspace-engine/pkg/oapi" + "workspace-engine/pkg/reconcile/events" "workspace-engine/pkg/workspace" "workspace-engine/pkg/workspace/releasemanager" "workspace-engine/pkg/workspace/releasemanager/trace" @@ -50,6 +51,16 @@ func getRelevantTargets(ctx context.Context, ws *workspace.Workspace, userApprov return releaseTargets, nil } +func enqueuePolicySummary(ctx context.Context, ws *workspace.Workspace, record *oapi.UserApprovalRecord) { + if err := events.EnqueuePolicySummary(ws.Queue(), ctx, events.EnvironmentVersionSummaryParams{ + WorkspaceID: ws.ID, + EnvironmentID: record.EnvironmentId, + VersionID: record.VersionId, + }.ToParams()); err != nil { + log.Error("failed to enqueue policy summary for approval", "error", err) + } +} + func HandleUserApprovalRecordCreated( ctx context.Context, ws *workspace.Workspace, @@ -87,6 +98,8 @@ func HandleUserApprovalRecordCreated( _ = ws.ReleaseManager().ReconcileTargets(ctx, relevantTargets, releasemanager.WithTrigger(trace.TriggerApprovalCreated)) + enqueuePolicySummary(ctx, ws, userApprovalRecord) + return nil } @@ -122,6 +135,8 @@ func HandleUserApprovalRecordUpdated( _ = ws.ReleaseManager().ReconcileTargets(ctx, relevantTargets, releasemanager.WithTrigger(trace.TriggerApprovalUpdated)) + enqueuePolicySummary(ctx, ws, userApprovalRecord) + return nil } @@ -157,5 +172,7 @@ func HandleUserApprovalRecordDeleted( _ = ws.ReleaseManager().ReconcileTargets(ctx, relevantTargets, releasemanager.WithTrigger(trace.TriggerApprovalUpdated)) + enqueuePolicySummary(ctx, ws, userApprovalRecord) + return nil } diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluators.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluators.go index 3a499df14..4de54c8bd 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluators.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluators.go @@ -37,31 +37,3 @@ func EvaluatorsForSummary(store *store.Store, rule *oapi.PolicyRule) []evaluator ) } -// EvaluatorsForEnvironmentSummary returns evaluators scoped to an environment -// (no version/resource/deployment needed). Used by the policy-summary controller -// for the "environment" scope channel. -func EvaluatorsForEnvironmentSummary(store *store.Store, rule *oapi.PolicyRule) []evaluator.Evaluator { - return evaluator.CollectEvaluators( - deploymentwindow.NewSummaryEvaluatorFromStore(store, rule), - ) -} - -// EvaluatorsForEnvironmentVersionSummary returns evaluators scoped to an -// (environment, version) pair. Used by the policy-summary controller for the -// "environment-version" scope channel. -func EvaluatorsForEnvironmentVersionSummary(store *store.Store, rule *oapi.PolicyRule) []evaluator.Evaluator { - return evaluator.CollectEvaluators( - approval.NewEvaluatorFromStore(store, rule), - environmentprogression.NewEvaluatorFromStore(store, rule), - gradualrollout.NewSummaryEvaluatorFromStore(store, rule), - ) -} - -// EvaluatorsForDeploymentVersionSummary returns evaluators scoped to a -// (deployment, version) pair. Used by the policy-summary controller for the -// "deployment-version" scope channel. -func EvaluatorsForDeploymentVersionSummary(store *store.Store, rule *oapi.PolicyRule) []evaluator.Evaluator { - return evaluator.CollectEvaluators( - versioncooldown.NewSummaryEvaluatorFromStore(store, rule), - ) -} diff --git a/apps/workspace-engine/svc/controllers/deploymentresourceselectoreval/controller.go b/apps/workspace-engine/svc/controllers/deploymentresourceselectoreval/controller.go index d408de6f8..81a86167d 100644 --- a/apps/workspace-engine/svc/controllers/deploymentresourceselectoreval/controller.go +++ b/apps/workspace-engine/svc/controllers/deploymentresourceselectoreval/controller.go @@ -89,6 +89,9 @@ func (c *Controller) Process(ctx context.Context, item reconcile.Item) (reconcil if err := c.enqueueReleaseTargets(ctx, deployment.WorkspaceID, releaseTargets); err != nil { return reconcile.Result{}, fmt.Errorf("enqueue release targets: %w", err) } + if err := c.enqueuePolicySummaries(ctx, deployment.WorkspaceID, releaseTargets); err != nil { + log.Error("failed to enqueue policy summaries", "error", err) + } } return reconcile.Result{}, nil @@ -112,6 +115,25 @@ func (c *Controller) enqueueReleaseTargets(ctx context.Context, workspaceID uuid return events.EnqueueManyDesiredRelease(c.queue, ctx, params) } +func (c *Controller) enqueuePolicySummaries(ctx context.Context, workspaceID uuid.UUID, releaseTargets []ReleaseTarget) error { + wsID := workspaceID.String() + seen := make(map[string]struct{}) + var params []events.PolicySummaryParams + + for _, rt := range releaseTargets { + key := rt.EnvironmentID.String() + if _, ok := seen[key]; !ok { + seen[key] = struct{}{} + params = append(params, events.EnvironmentSummaryParams{ + WorkspaceID: wsID, + EnvironmentID: rt.EnvironmentID.String(), + }.ToParams()) + } + } + + return events.EnqueueManyPolicySummary(c.queue, ctx, params) +} + // evalResources streams resources from the DB and evaluates the CEL selector // concurrently, returning the IDs of all matched resources. func (c *Controller) evalResources(ctx context.Context, deployment *DeploymentInfo, selector cel.Program) ([]uuid.UUID, error) { diff --git a/apps/workspace-engine/svc/controllers/policysummary/controller.go b/apps/workspace-engine/svc/controllers/policysummary/controller.go index 71ed97ae1..1fac29c4e 100644 --- a/apps/workspace-engine/svc/controllers/policysummary/controller.go +++ b/apps/workspace-engine/svc/controllers/policysummary/controller.go @@ -9,6 +9,7 @@ import ( "github.com/charmbracelet/log" + "workspace-engine/pkg/db" "workspace-engine/pkg/reconcile" "workspace-engine/pkg/reconcile/events" "workspace-engine/pkg/reconcile/postgres" @@ -77,9 +78,10 @@ func New(workerID string, pgxPool *pgxpool.Pool) svc.Service { kind := events.PolicySummaryKind queue := postgres.NewForKinds(pgxPool, kind) + queries := db.New(pgxPool) controller := &Controller{ - getter: &PostgresGetter{}, - setter: &PostgresSetter{}, + getter: NewPostgresGetter(queries), + setter: NewPostgresSetter(queries), } worker, err := reconcile.NewWorker( kind, diff --git a/apps/workspace-engine/svc/controllers/policysummary/getters.go b/apps/workspace-engine/svc/controllers/policysummary/getters.go index a9df6d1f9..62f7995d1 100644 --- a/apps/workspace-engine/svc/controllers/policysummary/getters.go +++ b/apps/workspace-engine/svc/controllers/policysummary/getters.go @@ -4,19 +4,17 @@ import ( "context" "workspace-engine/pkg/oapi" + "workspace-engine/svc/controllers/policysummary/summaryeval" "github.com/google/uuid" ) +type evalGetter = summaryeval.Getter + type Getter interface { - GetEnvironment(ctx context.Context, environmentID uuid.UUID) (*oapi.Environment, error) - GetDeployment(ctx context.Context, deploymentID uuid.UUID) (*oapi.Deployment, error) - GetVersion(ctx context.Context, versionID uuid.UUID) (*oapi.DeploymentVersion, error) + evalGetter + GetVersion(ctx context.Context, versionID uuid.UUID) (*oapi.DeploymentVersion, error) GetPoliciesForEnvironment(ctx context.Context, workspaceID, environmentID uuid.UUID) ([]*oapi.Policy, error) GetPoliciesForDeployment(ctx context.Context, workspaceID, deploymentID uuid.UUID) ([]*oapi.Policy, error) - - // TODO: These may need to be added or adapted from existing store methods. - // The summary evaluators use getters internally; these are for building the - // EvaluatorScope that gets passed to the evaluators. } diff --git a/apps/workspace-engine/svc/controllers/policysummary/getters_postgres.go b/apps/workspace-engine/svc/controllers/policysummary/getters_postgres.go index ecb28be1f..0190048d4 100644 --- a/apps/workspace-engine/svc/controllers/policysummary/getters_postgres.go +++ b/apps/workspace-engine/svc/controllers/policysummary/getters_postgres.go @@ -4,36 +4,64 @@ import ( "context" "fmt" + "workspace-engine/pkg/db" "workspace-engine/pkg/oapi" + "workspace-engine/svc/controllers/policysummary/summaryeval" "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" ) -type PostgresGetter struct{} +type summaryevalGetter = summaryeval.PostgresGetter var _ Getter = (*PostgresGetter)(nil) -func (g *PostgresGetter) GetEnvironment(ctx context.Context, environmentID uuid.UUID) (*oapi.Environment, error) { - // TODO: query environment by ID from postgres - return nil, fmt.Errorf("not implemented") +type PostgresGetter struct { + *summaryevalGetter + queries *db.Queries } -func (g *PostgresGetter) GetDeployment(ctx context.Context, deploymentID uuid.UUID) (*oapi.Deployment, error) { - // TODO: query deployment by ID from postgres - return nil, fmt.Errorf("not implemented") +func NewPostgresGetter(queries *db.Queries) *PostgresGetter { + return &PostgresGetter{ + summaryevalGetter: summaryeval.NewPostgresGetter(queries), + queries: queries, + } } func (g *PostgresGetter) GetVersion(ctx context.Context, versionID uuid.UUID) (*oapi.DeploymentVersion, error) { - // TODO: query deployment_version by ID from postgres - return nil, fmt.Errorf("not implemented") + ver, err := g.queries.GetDeploymentVersionByID(ctx, versionID) + if err != nil { + return nil, fmt.Errorf("get version %s: %w", versionID, err) + } + return db.ToOapiDeploymentVersion(ver), nil } func (g *PostgresGetter) GetPoliciesForEnvironment(ctx context.Context, workspaceID, environmentID uuid.UUID) ([]*oapi.Policy, error) { - // TODO: query policies whose selector matches this environment - return nil, fmt.Errorf("not implemented") + rows, err := g.queries.ListPoliciesByWorkspaceID(ctx, db.ListPoliciesByWorkspaceIDParams{ + WorkspaceID: workspaceID, + Limit: pgtype.Int4{Int32: 5000, Valid: true}, + }) + if err != nil { + return nil, fmt.Errorf("list policies for workspace %s: %w", workspaceID, err) + } + policies := make([]*oapi.Policy, 0, len(rows)) + for _, row := range rows { + policies = append(policies, db.ToOapiPolicy(row)) + } + return policies, nil } func (g *PostgresGetter) GetPoliciesForDeployment(ctx context.Context, workspaceID, deploymentID uuid.UUID) ([]*oapi.Policy, error) { - // TODO: query policies whose selector matches this deployment - return nil, fmt.Errorf("not implemented") + rows, err := g.queries.ListPoliciesByWorkspaceID(ctx, db.ListPoliciesByWorkspaceIDParams{ + WorkspaceID: workspaceID, + Limit: pgtype.Int4{Int32: 5000, Valid: true}, + }) + if err != nil { + return nil, fmt.Errorf("list policies for workspace %s: %w", workspaceID, err) + } + policies := make([]*oapi.Policy, 0, len(rows)) + for _, row := range rows { + policies = append(policies, db.ToOapiPolicy(row)) + } + return policies, nil } diff --git a/apps/workspace-engine/svc/controllers/policysummary/getters_postgres_test.go b/apps/workspace-engine/svc/controllers/policysummary/getters_postgres_test.go new file mode 100644 index 000000000..c45e9c36f --- /dev/null +++ b/apps/workspace-engine/svc/controllers/policysummary/getters_postgres_test.go @@ -0,0 +1,235 @@ +package policysummary_test + +import ( + "context" + "os" + "testing" + + "workspace-engine/pkg/db" + "workspace-engine/svc/controllers/policysummary" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const defaultDBURL = "postgresql://ctrlplane:ctrlplane@localhost:5432/ctrlplane" + +func requireTestDB(t *testing.T) *pgxpool.Pool { + t.Helper() + if os.Getenv("USE_DATABASE_BACKING") == "" { + t.Skip("Skipping: set USE_DATABASE_BACKING=1 to run DB-backed tests") + } + if os.Getenv("POSTGRES_URL") == "" { + os.Setenv("POSTGRES_URL", defaultDBURL) + } + ctx := context.Background() + pool := db.GetPool(ctx) + if err := pool.Ping(ctx); err != nil { + t.Skipf("Database not available: %v", err) + } + return pool +} + +type fixture struct { + pool *pgxpool.Pool + workspaceID uuid.UUID + deploymentID uuid.UUID + environmentID uuid.UUID + resourceID uuid.UUID + providerID uuid.UUID +} + +func setupFixture(t *testing.T, pool *pgxpool.Pool) *fixture { + t.Helper() + ctx := context.Background() + + f := &fixture{ + pool: pool, + workspaceID: uuid.New(), + deploymentID: uuid.New(), + environmentID: uuid.New(), + resourceID: uuid.New(), + providerID: uuid.New(), + } + + slug := "test-ps-" + f.workspaceID.String()[:8] + _, err := pool.Exec(ctx, + "INSERT INTO workspace (id, name, slug) VALUES ($1, $2, $3)", + f.workspaceID, "test_policy_summary", slug) + require.NoError(t, err) + + _, err = pool.Exec(ctx, + "INSERT INTO resource_provider (id, name, workspace_id) VALUES ($1, $2, $3)", + f.providerID, "test-provider", f.workspaceID) + require.NoError(t, err) + + _, err = pool.Exec(ctx, + `INSERT INTO deployment (id, name, description, resource_selector, workspace_id) + VALUES ($1, $2, $3, $4, $5)`, + f.deploymentID, "test-deploy", "", "true", f.workspaceID) + require.NoError(t, err) + + _, err = pool.Exec(ctx, + `INSERT INTO environment (id, name, resource_selector, workspace_id) + VALUES ($1, $2, $3, $4)`, + f.environmentID, "test-env", "true", f.workspaceID) + require.NoError(t, err) + + t.Cleanup(func() { + cleanCtx := context.Background() + _, _ = pool.Exec(cleanCtx, "DELETE FROM deployment_version WHERE workspace_id = $1", f.workspaceID) + _, _ = pool.Exec(cleanCtx, "DELETE FROM policy WHERE workspace_id = $1", f.workspaceID) + _, _ = pool.Exec(cleanCtx, "DELETE FROM deployment WHERE workspace_id = $1", f.workspaceID) + _, _ = pool.Exec(cleanCtx, "DELETE FROM environment WHERE workspace_id = $1", f.workspaceID) + _, _ = pool.Exec(cleanCtx, "DELETE FROM resource_provider WHERE workspace_id = $1", f.workspaceID) + _, _ = pool.Exec(cleanCtx, "DELETE FROM workspace WHERE id = $1", f.workspaceID) + }) + + return f +} + +func TestPostgresGetter_GetEnvironment(t *testing.T) { + pool := requireTestDB(t) + f := setupFixture(t, pool) + ctx := context.Background() + + queries := db.New(pool) + getter := policysummary.NewPostgresGetter(queries) + + env, err := getter.GetEnvironment(ctx, f.environmentID.String()) + require.NoError(t, err) + require.NotNil(t, env) + assert.Equal(t, f.environmentID.String(), env.Id) + assert.Equal(t, "test-env", env.Name) +} + +func TestPostgresGetter_GetEnvironment_NotFound(t *testing.T) { + pool := requireTestDB(t) + _ = setupFixture(t, pool) + ctx := context.Background() + + queries := db.New(pool) + getter := policysummary.NewPostgresGetter(queries) + + _, err := getter.GetEnvironment(ctx, uuid.New().String()) + assert.Error(t, err) +} + +func TestPostgresGetter_GetDeployment(t *testing.T) { + pool := requireTestDB(t) + f := setupFixture(t, pool) + ctx := context.Background() + + queries := db.New(pool) + getter := policysummary.NewPostgresGetter(queries) + + dep, err := getter.GetDeployment(ctx, f.deploymentID.String()) + require.NoError(t, err) + require.NotNil(t, dep) + assert.Equal(t, f.deploymentID.String(), dep.Id) + assert.Equal(t, "test-deploy", dep.Name) +} + +func TestPostgresGetter_GetDeployment_NotFound(t *testing.T) { + pool := requireTestDB(t) + _ = setupFixture(t, pool) + ctx := context.Background() + + queries := db.New(pool) + getter := policysummary.NewPostgresGetter(queries) + + _, err := getter.GetDeployment(ctx, uuid.New().String()) + assert.Error(t, err) +} + +func TestPostgresGetter_GetVersion(t *testing.T) { + pool := requireTestDB(t) + f := setupFixture(t, pool) + ctx := context.Background() + + versionID := uuid.New() + _, err := pool.Exec(ctx, + `INSERT INTO deployment_version (id, name, tag, deployment_id, status, workspace_id) + VALUES ($1, $2, $3, $4, 'ready', $5)`, + versionID, "v1.0.0", "v1.0.0", f.deploymentID, f.workspaceID) + require.NoError(t, err) + + queries := db.New(pool) + getter := policysummary.NewPostgresGetter(queries) + + ver, err := getter.GetVersion(ctx, versionID) + require.NoError(t, err) + require.NotNil(t, ver) + assert.Equal(t, versionID.String(), ver.Id) + assert.Equal(t, "v1.0.0", ver.Tag) +} + +func TestPostgresGetter_GetVersion_NotFound(t *testing.T) { + pool := requireTestDB(t) + _ = setupFixture(t, pool) + ctx := context.Background() + + queries := db.New(pool) + getter := policysummary.NewPostgresGetter(queries) + + _, err := getter.GetVersion(ctx, uuid.New()) + assert.Error(t, err) +} + +func TestPostgresGetter_GetPoliciesForEnvironment(t *testing.T) { + pool := requireTestDB(t) + f := setupFixture(t, pool) + ctx := context.Background() + + policyID := uuid.New() + _, err := pool.Exec(ctx, + `INSERT INTO policy (id, name, selector, priority, enabled, workspace_id) + VALUES ($1, $2, $3, $4, $5, $6)`, + policyID, "test-policy", "true", 10, true, f.workspaceID) + require.NoError(t, err) + + queries := db.New(pool) + getter := policysummary.NewPostgresGetter(queries) + + policies, err := getter.GetPoliciesForEnvironment(ctx, f.workspaceID, f.environmentID) + require.NoError(t, err) + require.Len(t, policies, 1) + assert.Equal(t, policyID.String(), policies[0].Id) + assert.Equal(t, "test-policy", policies[0].Name) +} + +func TestPostgresGetter_GetPoliciesForDeployment(t *testing.T) { + pool := requireTestDB(t) + f := setupFixture(t, pool) + ctx := context.Background() + + policyID := uuid.New() + _, err := pool.Exec(ctx, + `INSERT INTO policy (id, name, selector, priority, enabled, workspace_id) + VALUES ($1, $2, $3, $4, $5, $6)`, + policyID, "dep-policy", "true", 5, true, f.workspaceID) + require.NoError(t, err) + + queries := db.New(pool) + getter := policysummary.NewPostgresGetter(queries) + + policies, err := getter.GetPoliciesForDeployment(ctx, f.workspaceID, f.deploymentID) + require.NoError(t, err) + require.Len(t, policies, 1) + assert.Equal(t, policyID.String(), policies[0].Id) +} + +func TestPostgresGetter_GetPoliciesEmpty(t *testing.T) { + pool := requireTestDB(t) + f := setupFixture(t, pool) + ctx := context.Background() + + queries := db.New(pool) + getter := policysummary.NewPostgresGetter(queries) + + policies, err := getter.GetPoliciesForEnvironment(ctx, f.workspaceID, f.environmentID) + require.NoError(t, err) + assert.Empty(t, policies) +} diff --git a/apps/workspace-engine/svc/controllers/policysummary/reconcile.go b/apps/workspace-engine/svc/controllers/policysummary/reconcile.go index 6fe42e3fb..ae6ca273f 100644 --- a/apps/workspace-engine/svc/controllers/policysummary/reconcile.go +++ b/apps/workspace-engine/svc/controllers/policysummary/reconcile.go @@ -8,6 +8,7 @@ import ( "workspace-engine/pkg/oapi" "workspace-engine/pkg/reconcile/events" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator" + "workspace-engine/svc/controllers/policysummary/summaryeval" "github.com/google/uuid" ) @@ -22,13 +23,11 @@ type reconciler struct { setter Setter } -// reconcileEnvironment handles the "environment" scope channel. -// Runs: deployment window evaluator. func (r *reconciler) reconcileEnvironment(ctx context.Context, scope *EnvironmentScope) (*ReconcileResult, error) { ctx, span := tracer.Start(ctx, "policysummary.reconcileEnvironment") defer span.End() - env, err := r.getter.GetEnvironment(ctx, scope.EnvironmentID) + env, err := r.getter.GetEnvironment(ctx, scope.EnvironmentID.String()) if err != nil { return nil, fmt.Errorf("get environment: %w", err) } @@ -43,8 +42,7 @@ func (r *reconciler) reconcileEnvironment(ctx context.Context, scope *Environmen } rows, nextTime := r.evaluateAndCollect(ctx, policies, evalScope, func(rule *oapi.PolicyRule) []evaluator.Evaluator { - // TODO: return only environment-scoped summary evaluators (deployment window) - return nil + return summaryeval.EnvironmentRuleEvaluators(rule) }) for i := range rows { @@ -58,13 +56,11 @@ func (r *reconciler) reconcileEnvironment(ctx context.Context, scope *Environmen return &ReconcileResult{NextReconcileAt: nextTime}, nil } -// reconcileEnvironmentVersion handles the "environment-version" scope channel. -// Runs: approval, environment progression, gradual rollout evaluators. func (r *reconciler) reconcileEnvironmentVersion(ctx context.Context, scope *EnvironmentVersionScope) (*ReconcileResult, error) { ctx, span := tracer.Start(ctx, "policysummary.reconcileEnvironmentVersion") defer span.End() - env, err := r.getter.GetEnvironment(ctx, scope.EnvironmentID) + env, err := r.getter.GetEnvironment(ctx, scope.EnvironmentID.String()) if err != nil { return nil, fmt.Errorf("get environment: %w", err) } @@ -85,9 +81,7 @@ func (r *reconciler) reconcileEnvironmentVersion(ctx context.Context, scope *Env } rows, nextTime := r.evaluateAndCollect(ctx, policies, evalScope, func(rule *oapi.PolicyRule) []evaluator.Evaluator { - // TODO: return environment-version-scoped summary evaluators - // (approval, environment progression, gradual rollout) - return nil + return summaryeval.EnvironmentVersionRuleEvaluators(r.getter, rule) }) for i := range rows { @@ -102,13 +96,11 @@ func (r *reconciler) reconcileEnvironmentVersion(ctx context.Context, scope *Env return &ReconcileResult{NextReconcileAt: nextTime}, nil } -// reconcileDeploymentVersion handles the "deployment-version" scope channel. -// Runs: version cooldown evaluator. func (r *reconciler) reconcileDeploymentVersion(ctx context.Context, scope *DeploymentVersionScope) (*ReconcileResult, error) { ctx, span := tracer.Start(ctx, "policysummary.reconcileDeploymentVersion") defer span.End() - deployment, err := r.getter.GetDeployment(ctx, scope.DeploymentID) + deployment, err := r.getter.GetDeployment(ctx, scope.DeploymentID.String()) if err != nil { return nil, fmt.Errorf("get deployment: %w", err) } @@ -129,8 +121,7 @@ func (r *reconciler) reconcileDeploymentVersion(ctx context.Context, scope *Depl } rows, nextTime := r.evaluateAndCollect(ctx, policies, evalScope, func(rule *oapi.PolicyRule) []evaluator.Evaluator { - // TODO: return deployment-version-scoped summary evaluators (version cooldown) - return nil + return summaryeval.DeploymentVersionRuleEvaluators(r.getter, rule) }) for i := range rows { @@ -145,9 +136,6 @@ func (r *reconciler) reconcileDeploymentVersion(ctx context.Context, scope *Depl return &ReconcileResult{NextReconcileAt: nextTime}, nil } -// evaluateAndCollect runs the given evaluator factory against all policies and -// collects RuleSummaryRows. Returns the rows and the earliest NextEvaluationTime -// across all evaluations (for RequeueAfter). func (r *reconciler) evaluateAndCollect( ctx context.Context, policies []*oapi.Policy, @@ -170,9 +158,8 @@ func (r *reconciler) evaluateAndCollect( result := eval.Evaluate(ctx, scope) rows = append(rows, RuleSummaryRow{ - WorkspaceID: r.workspaceID, - RuleID: uuid.MustParse(rule.Id), - Evaluation: result, + RuleID: uuid.MustParse(rule.Id), + Evaluation: result, }) if result.NextEvaluationTime != nil { diff --git a/apps/workspace-engine/svc/controllers/policysummary/setters.go b/apps/workspace-engine/svc/controllers/policysummary/setters.go index 3f4771ca6..9640afde3 100644 --- a/apps/workspace-engine/svc/controllers/policysummary/setters.go +++ b/apps/workspace-engine/svc/controllers/policysummary/setters.go @@ -10,7 +10,6 @@ import ( // RuleSummaryRow is the DB representation of a single rule evaluation result. type RuleSummaryRow struct { - WorkspaceID uuid.UUID RuleID uuid.UUID DeploymentID *uuid.UUID EnvironmentID *uuid.UUID diff --git a/apps/workspace-engine/svc/controllers/policysummary/setters_postgres.go b/apps/workspace-engine/svc/controllers/policysummary/setters_postgres.go index ab1907395..be7ae8c57 100644 --- a/apps/workspace-engine/svc/controllers/policysummary/setters_postgres.go +++ b/apps/workspace-engine/svc/controllers/policysummary/setters_postgres.go @@ -3,18 +3,80 @@ package policysummary import ( "context" "fmt" -) -type PostgresSetter struct{} + "workspace-engine/pkg/db" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" +) var _ Setter = (*PostgresSetter)(nil) +type PostgresSetter struct { + queries *db.Queries +} + +func NewPostgresSetter(queries *db.Queries) *PostgresSetter { + return &PostgresSetter{queries: queries} +} + +func uuidOrZero(id *uuid.UUID) uuid.UUID { + if id != nil { + return *id + } + return uuid.Nil +} + func (s *PostgresSetter) UpsertRuleSummaries(ctx context.Context, rows []RuleSummaryRow) error { - // TODO: batch upsert into policy_rule_summary table - // ON CONFLICT (rule_id, deployment_id, environment_id, version_id) DO UPDATE - // SET allowed = EXCLUDED.allowed, message = EXCLUDED.message, ... if len(rows) == 0 { return nil } - return fmt.Errorf("not implemented") + + params := make([]db.UpsertPolicyRuleSummaryParams, len(rows)) + for i, row := range rows { + eval := row.Evaluation + + var actionType pgtype.Text + if eval.ActionType != nil { + actionType = pgtype.Text{String: string(*eval.ActionType), Valid: true} + } + + detailsMap := map[string]any{} + if eval.Details != nil { + detailsMap = eval.Details + } + + var satisfiedAt pgtype.Timestamptz + if eval.SatisfiedAt != nil { + satisfiedAt = pgtype.Timestamptz{Time: *eval.SatisfiedAt, Valid: true} + } + + var nextEvalAt pgtype.Timestamptz + if eval.NextEvaluationTime != nil { + nextEvalAt = pgtype.Timestamptz{Time: *eval.NextEvaluationTime, Valid: true} + } + + params[i] = db.UpsertPolicyRuleSummaryParams{ + RuleID: row.RuleID, + DeploymentID: uuidOrZero(row.DeploymentID), + EnvironmentID: uuidOrZero(row.EnvironmentID), + VersionID: uuidOrZero(row.VersionID), + Allowed: eval.Allowed, + ActionRequired: eval.ActionRequired, + ActionType: actionType, + Message: eval.Message, + Details: detailsMap, + SatisfiedAt: satisfiedAt, + NextEvaluationAt: nextEvalAt, + } + } + + results := s.queries.UpsertPolicyRuleSummary(ctx, params) + var batchErr error + results.Exec(func(i int, err error) { + if err != nil && batchErr == nil { + batchErr = fmt.Errorf("upsert rule summary %d: %w", i, err) + } + }) + return batchErr } diff --git a/apps/workspace-engine/svc/controllers/policysummary/summaryeval/getter.go b/apps/workspace-engine/svc/controllers/policysummary/summaryeval/getter.go new file mode 100644 index 000000000..6848f319f --- /dev/null +++ b/apps/workspace-engine/svc/controllers/policysummary/summaryeval/getter.go @@ -0,0 +1,20 @@ +package summaryeval + +import ( + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/approval" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versioncooldown" +) + +type approvalGetter = approval.Getters +type environmentProgressionGetter = environmentprogression.Getters +type gradualRolloutGetter = gradualrollout.Getters +type versionCooldownGetter = versioncooldown.Getters + +type Getter interface { + approvalGetter + environmentProgressionGetter + gradualRolloutGetter + versionCooldownGetter +} diff --git a/apps/workspace-engine/svc/controllers/policysummary/summaryeval/getter_postgres.go b/apps/workspace-engine/svc/controllers/policysummary/summaryeval/getter_postgres.go new file mode 100644 index 000000000..6e29ab043 --- /dev/null +++ b/apps/workspace-engine/svc/controllers/policysummary/summaryeval/getter_postgres.go @@ -0,0 +1,39 @@ +package summaryeval + +import ( + "workspace-engine/pkg/db" + "workspace-engine/pkg/oapi" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versioncooldown" +) + +var _ Getter = (*PostgresGetter)(nil) + +type PostgresGetter struct { + gradualRolloutGetter + versioncooldown *versioncooldown.PostgresGetters +} + +func NewPostgresGetter(queries *db.Queries) *PostgresGetter { + return &PostgresGetter{ + gradualRolloutGetter: gradualrollout.NewPostgresGetters(queries), + versioncooldown: versioncooldown.NewPostgresGetters(queries), + } +} + +func (g *PostgresGetter) GetJobVerificationStatus(jobID string) oapi.JobVerificationStatus { + return g.versioncooldown.GetJobVerificationStatus(jobID) +} + +func (g *PostgresGetter) NewVersionCooldownEvaluator(rule *oapi.PolicyRule) evaluator.Evaluator { + return g.versioncooldown.NewVersionCooldownEvaluator(rule) +} + +func (g *PostgresGetter) GetReleaseTargets() ([]*oapi.ReleaseTarget, error) { + return g.versioncooldown.GetReleaseTargets() +} + +func (g *PostgresGetter) GetJobsForReleaseTarget(releaseTarget *oapi.ReleaseTarget) map[string]*oapi.Job { + return g.versioncooldown.GetJobsForReleaseTarget(releaseTarget) +} diff --git a/apps/workspace-engine/svc/controllers/policysummary/summaryeval/summaryeval.go b/apps/workspace-engine/svc/controllers/policysummary/summaryeval/summaryeval.go new file mode 100644 index 000000000..a89d5214f --- /dev/null +++ b/apps/workspace-engine/svc/controllers/policysummary/summaryeval/summaryeval.go @@ -0,0 +1,29 @@ +package summaryeval + +import ( + "workspace-engine/pkg/oapi" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/approval" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentwindow" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versioncooldown" +) + +func EnvironmentRuleEvaluators(rule *oapi.PolicyRule) []evaluator.Evaluator { + return evaluator.CollectEvaluators( + deploymentwindow.NewSummaryEvaluator(rule), + ) +} + +func EnvironmentVersionRuleEvaluators(getter Getter, rule *oapi.PolicyRule) []evaluator.Evaluator { + return evaluator.CollectEvaluators( + approval.NewEvaluator(getter, rule), + environmentprogression.NewEvaluator(getter, rule), + ) +} + +func DeploymentVersionRuleEvaluators(getter Getter, rule *oapi.PolicyRule) []evaluator.Evaluator { + return evaluator.CollectEvaluators( + versioncooldown.NewSummaryEvaluator(getter, rule), + ) +} diff --git a/apps/workspace-engine/svc/controllers/policysummary/summaryeval/summaryeval_test.go b/apps/workspace-engine/svc/controllers/policysummary/summaryeval/summaryeval_test.go new file mode 100644 index 000000000..f48fa2053 --- /dev/null +++ b/apps/workspace-engine/svc/controllers/policysummary/summaryeval/summaryeval_test.go @@ -0,0 +1,403 @@ +package summaryeval + +import ( + "context" + "testing" + + "workspace-engine/pkg/oapi" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// --------------------------------------------------------------------------- +// Mock getter satisfying the composite summaryeval.Getter interface +// --------------------------------------------------------------------------- + +var _ Getter = (*mockGetter)(nil) + +type mockGetter struct{} + +func (m *mockGetter) GetApprovalRecords(_ context.Context, _, _ string) ([]*oapi.UserApprovalRecord, error) { + return nil, nil +} +func (m *mockGetter) GetEnvironment(_ context.Context, _ string) (*oapi.Environment, error) { + return nil, nil +} +func (m *mockGetter) GetAllEnvironments(_ context.Context, _ string) (map[string]*oapi.Environment, error) { + return nil, nil +} +func (m *mockGetter) GetDeployment(_ context.Context, _ string) (*oapi.Deployment, error) { + return nil, nil +} +func (m *mockGetter) GetAllDeployments(_ context.Context, _ string) (map[string]*oapi.Deployment, error) { + return nil, nil +} +func (m *mockGetter) GetResource(_ context.Context, _ string) (*oapi.Resource, error) { + return nil, nil +} +func (m *mockGetter) GetRelease(_ context.Context, _ string) (*oapi.Release, error) { + return nil, nil +} +func (m *mockGetter) GetSystemIDsForEnvironment(_ string) []string { return nil } +func (m *mockGetter) GetReleaseTargetsForEnvironment(_ context.Context, _ string) ([]*oapi.ReleaseTarget, error) { + return nil, nil +} +func (m *mockGetter) GetReleaseTargetsForDeployment(_ context.Context, _ string) ([]*oapi.ReleaseTarget, error) { + return nil, nil +} +func (m *mockGetter) GetJobsForReleaseTarget(_ *oapi.ReleaseTarget) map[string]*oapi.Job { + return nil +} +func (m *mockGetter) GetAllPolicies(_ context.Context, _ string) (map[string]*oapi.Policy, error) { + return nil, nil +} +func (m *mockGetter) GetPoliciesForReleaseTarget(_ context.Context, _ *oapi.ReleaseTarget) ([]*oapi.Policy, error) { + return nil, nil +} +func (m *mockGetter) GetPolicySkips(_ context.Context, _, _, _ string) ([]*oapi.PolicySkip, error) { + return nil, nil +} +func (m *mockGetter) HasCurrentRelease(_ context.Context, _ *oapi.ReleaseTarget) (bool, error) { + return false, nil +} +func (m *mockGetter) GetReleaseTargets() ([]*oapi.ReleaseTarget, error) { return nil, nil } +func (m *mockGetter) GetJobVerificationStatus(_ string) oapi.JobVerificationStatus { + return oapi.JobVerificationStatusCancelled +} +func (m *mockGetter) NewVersionCooldownEvaluator(rule *oapi.PolicyRule) evaluator.Evaluator { + return nil +} + +// --------------------------------------------------------------------------- +// Rule builder helpers +// --------------------------------------------------------------------------- + +func ruleWithDeploymentWindow(id string) *oapi.PolicyRule { + return &oapi.PolicyRule{ + Id: id, + DeploymentWindow: &oapi.DeploymentWindowRule{ + Rrule: "FREQ=WEEKLY;BYDAY=MO;BYHOUR=9", + DurationMinutes: 60, + }, + } +} + +func ruleWithApproval(id string) *oapi.PolicyRule { + return &oapi.PolicyRule{ + Id: id, + AnyApproval: &oapi.AnyApprovalRule{MinApprovals: 1}, + } +} + +func ruleWithEnvironmentProgression(id string) *oapi.PolicyRule { + return &oapi.PolicyRule{ + Id: id, + EnvironmentProgression: &oapi.EnvironmentProgressionRule{}, + } +} + +func ruleWithVersionCooldown(id string) *oapi.PolicyRule { + return &oapi.PolicyRule{ + Id: id, + VersionCooldown: &oapi.VersionCooldownRule{IntervalSeconds: 300}, + } +} + +func emptyRule(id string) *oapi.PolicyRule { + return &oapi.PolicyRule{Id: id} +} + +func evalTypes(evals []evaluator.Evaluator) []string { + types := make([]string, len(evals)) + for i, e := range evals { + types[i] = e.RuleType() + } + return types +} + +// --------------------------------------------------------------------------- +// EnvironmentRuleEvaluators tests +// --------------------------------------------------------------------------- + +func TestEnvironmentRuleEvaluators(t *testing.T) { + t.Run("returns empty for nil rule", func(t *testing.T) { + evals := EnvironmentRuleEvaluators(nil) + assert.Empty(t, evals) + }) + + t.Run("returns empty for rule without deployment window", func(t *testing.T) { + evals := EnvironmentRuleEvaluators(emptyRule("r-1")) + assert.Empty(t, evals) + }) + + t.Run("returns empty for rule with only approval", func(t *testing.T) { + evals := EnvironmentRuleEvaluators(ruleWithApproval("r-1")) + assert.Empty(t, evals) + }) + + t.Run("returns empty for rule with only version cooldown", func(t *testing.T) { + evals := EnvironmentRuleEvaluators(ruleWithVersionCooldown("r-1")) + assert.Empty(t, evals) + }) + + t.Run("returns empty for rule with only environment progression", func(t *testing.T) { + evals := EnvironmentRuleEvaluators(ruleWithEnvironmentProgression("r-1")) + assert.Empty(t, evals) + }) + + t.Run("returns deployment window evaluator", func(t *testing.T) { + evals := EnvironmentRuleEvaluators(ruleWithDeploymentWindow("r-1")) + require.Len(t, evals, 1) + assert.Equal(t, evaluator.RuleTypeDeploymentWindow, evals[0].RuleType()) + }) + + t.Run("deployment window evaluator has correct scope fields", func(t *testing.T) { + evals := EnvironmentRuleEvaluators(ruleWithDeploymentWindow("r-1")) + require.Len(t, evals, 1) + assert.NotZero(t, evals[0].ScopeFields()&evaluator.ScopeEnvironment) + }) + + t.Run("returns deployment window even when other rule types present", func(t *testing.T) { + rule := &oapi.PolicyRule{ + Id: "r-1", + AnyApproval: &oapi.AnyApprovalRule{MinApprovals: 2}, + DeploymentWindow: &oapi.DeploymentWindowRule{ + Rrule: "FREQ=DAILY;BYHOUR=10", + DurationMinutes: 120, + }, + VersionCooldown: &oapi.VersionCooldownRule{IntervalSeconds: 60}, + } + evals := EnvironmentRuleEvaluators(rule) + require.Len(t, evals, 1) + assert.Equal(t, evaluator.RuleTypeDeploymentWindow, evals[0].RuleType()) + }) + + t.Run("returns empty for deployment window with invalid rrule", func(t *testing.T) { + rule := &oapi.PolicyRule{ + Id: "r-1", + DeploymentWindow: &oapi.DeploymentWindowRule{ + Rrule: "INVALID-RRULE-STRING", + DurationMinutes: 60, + }, + } + evals := EnvironmentRuleEvaluators(rule) + assert.Empty(t, evals) + }) +} + +// --------------------------------------------------------------------------- +// EnvironmentVersionRuleEvaluators tests +// --------------------------------------------------------------------------- + +func TestEnvironmentVersionRuleEvaluators(t *testing.T) { + getter := &mockGetter{} + + t.Run("returns empty for nil rule", func(t *testing.T) { + evals := EnvironmentVersionRuleEvaluators(getter, nil) + assert.Empty(t, evals) + }) + + t.Run("returns empty for nil getter", func(t *testing.T) { + evals := EnvironmentVersionRuleEvaluators(nil, ruleWithApproval("r-1")) + assert.Empty(t, evals) + }) + + t.Run("returns empty for nil getter and nil rule", func(t *testing.T) { + evals := EnvironmentVersionRuleEvaluators(nil, nil) + assert.Empty(t, evals) + }) + + t.Run("returns empty for rule without relevant fields", func(t *testing.T) { + evals := EnvironmentVersionRuleEvaluators(getter, emptyRule("r-1")) + assert.Empty(t, evals) + }) + + t.Run("returns empty for rule with only deployment window", func(t *testing.T) { + evals := EnvironmentVersionRuleEvaluators(getter, ruleWithDeploymentWindow("r-1")) + assert.Empty(t, evals) + }) + + t.Run("returns empty for rule with only version cooldown", func(t *testing.T) { + evals := EnvironmentVersionRuleEvaluators(getter, ruleWithVersionCooldown("r-1")) + assert.Empty(t, evals) + }) + + t.Run("returns approval evaluator", func(t *testing.T) { + evals := EnvironmentVersionRuleEvaluators(getter, ruleWithApproval("r-1")) + require.Len(t, evals, 1) + assert.Equal(t, evaluator.RuleTypeApproval, evals[0].RuleType()) + }) + + t.Run("returns environment progression evaluator", func(t *testing.T) { + evals := EnvironmentVersionRuleEvaluators(getter, ruleWithEnvironmentProgression("r-1")) + require.Len(t, evals, 1) + assert.Equal(t, evaluator.RuleTypeEnvironmentProgression, evals[0].RuleType()) + }) + + t.Run("returns both approval and environment progression when both present", func(t *testing.T) { + rule := &oapi.PolicyRule{ + Id: "r-1", + AnyApproval: &oapi.AnyApprovalRule{MinApprovals: 1}, + EnvironmentProgression: &oapi.EnvironmentProgressionRule{}, + } + evals := EnvironmentVersionRuleEvaluators(getter, rule) + require.Len(t, evals, 2) + types := evalTypes(evals) + assert.Contains(t, types, evaluator.RuleTypeApproval) + assert.Contains(t, types, evaluator.RuleTypeEnvironmentProgression) + }) + + t.Run("ignores deployment window and version cooldown in same rule", func(t *testing.T) { + rule := &oapi.PolicyRule{ + Id: "r-1", + AnyApproval: &oapi.AnyApprovalRule{MinApprovals: 1}, + DeploymentWindow: &oapi.DeploymentWindowRule{ + Rrule: "FREQ=WEEKLY;BYDAY=MO", + DurationMinutes: 30, + }, + VersionCooldown: &oapi.VersionCooldownRule{IntervalSeconds: 60}, + } + evals := EnvironmentVersionRuleEvaluators(getter, rule) + types := evalTypes(evals) + assert.NotContains(t, types, evaluator.RuleTypeDeploymentWindow) + assert.NotContains(t, types, evaluator.RuleTypeVersionCooldown) + assert.Contains(t, types, evaluator.RuleTypeApproval) + }) + + t.Run("approval evaluator scope includes environment and version", func(t *testing.T) { + evals := EnvironmentVersionRuleEvaluators(getter, ruleWithApproval("r-1")) + require.Len(t, evals, 1) + sf := evals[0].ScopeFields() + assert.NotZero(t, sf&evaluator.ScopeEnvironment) + assert.NotZero(t, sf&evaluator.ScopeVersion) + }) +} + +// --------------------------------------------------------------------------- +// DeploymentVersionRuleEvaluators tests +// --------------------------------------------------------------------------- + +func TestDeploymentVersionRuleEvaluators(t *testing.T) { + getter := &mockGetter{} + + t.Run("returns empty for nil rule", func(t *testing.T) { + evals := DeploymentVersionRuleEvaluators(getter, nil) + assert.Empty(t, evals) + }) + + t.Run("returns empty for nil getter", func(t *testing.T) { + evals := DeploymentVersionRuleEvaluators(nil, ruleWithVersionCooldown("r-1")) + assert.Empty(t, evals) + }) + + t.Run("returns empty for nil getter and nil rule", func(t *testing.T) { + evals := DeploymentVersionRuleEvaluators(nil, nil) + assert.Empty(t, evals) + }) + + t.Run("returns empty for rule without relevant fields", func(t *testing.T) { + evals := DeploymentVersionRuleEvaluators(getter, emptyRule("r-1")) + assert.Empty(t, evals) + }) + + t.Run("returns empty for rule with only deployment window", func(t *testing.T) { + evals := DeploymentVersionRuleEvaluators(getter, ruleWithDeploymentWindow("r-1")) + assert.Empty(t, evals) + }) + + t.Run("returns empty for rule with only approval", func(t *testing.T) { + evals := DeploymentVersionRuleEvaluators(getter, ruleWithApproval("r-1")) + assert.Empty(t, evals) + }) + + t.Run("returns empty for rule with only environment progression", func(t *testing.T) { + evals := DeploymentVersionRuleEvaluators(getter, ruleWithEnvironmentProgression("r-1")) + assert.Empty(t, evals) + }) + + t.Run("returns version cooldown evaluator", func(t *testing.T) { + evals := DeploymentVersionRuleEvaluators(getter, ruleWithVersionCooldown("r-1")) + require.Len(t, evals, 1) + assert.Equal(t, evaluator.RuleTypeVersionCooldown, evals[0].RuleType()) + }) + + t.Run("version cooldown evaluator scope includes version", func(t *testing.T) { + evals := DeploymentVersionRuleEvaluators(getter, ruleWithVersionCooldown("r-1")) + require.Len(t, evals, 1) + assert.NotZero(t, evals[0].ScopeFields()&evaluator.ScopeVersion) + }) + + t.Run("ignores deployment window and approval in same rule", func(t *testing.T) { + rule := &oapi.PolicyRule{ + Id: "r-1", + AnyApproval: &oapi.AnyApprovalRule{MinApprovals: 1}, + DeploymentWindow: &oapi.DeploymentWindowRule{ + Rrule: "FREQ=WEEKLY;BYDAY=MO", + DurationMinutes: 30, + }, + VersionCooldown: &oapi.VersionCooldownRule{IntervalSeconds: 300}, + } + evals := DeploymentVersionRuleEvaluators(getter, rule) + types := evalTypes(evals) + assert.NotContains(t, types, evaluator.RuleTypeDeploymentWindow) + assert.NotContains(t, types, evaluator.RuleTypeApproval) + assert.Contains(t, types, evaluator.RuleTypeVersionCooldown) + }) +} + +// --------------------------------------------------------------------------- +// Cross-function isolation: each scope function only returns its own evaluators +// --------------------------------------------------------------------------- + +func TestScopeIsolation(t *testing.T) { + getter := &mockGetter{} + + rule := &oapi.PolicyRule{ + Id: "r-all", + AnyApproval: &oapi.AnyApprovalRule{MinApprovals: 1}, + DeploymentWindow: &oapi.DeploymentWindowRule{ + Rrule: "FREQ=DAILY;BYHOUR=9", + DurationMinutes: 60, + }, + EnvironmentProgression: &oapi.EnvironmentProgressionRule{}, + VersionCooldown: &oapi.VersionCooldownRule{IntervalSeconds: 300}, + } + + t.Run("environment scope only returns deployment window", func(t *testing.T) { + evals := EnvironmentRuleEvaluators(rule) + types := evalTypes(evals) + assert.Equal(t, []string{evaluator.RuleTypeDeploymentWindow}, types) + }) + + t.Run("environment-version scope only returns approval and env progression", func(t *testing.T) { + evals := EnvironmentVersionRuleEvaluators(getter, rule) + types := evalTypes(evals) + assert.Len(t, types, 2) + assert.Contains(t, types, evaluator.RuleTypeApproval) + assert.Contains(t, types, evaluator.RuleTypeEnvironmentProgression) + assert.NotContains(t, types, evaluator.RuleTypeDeploymentWindow) + assert.NotContains(t, types, evaluator.RuleTypeVersionCooldown) + }) + + t.Run("deployment-version scope only returns version cooldown", func(t *testing.T) { + evals := DeploymentVersionRuleEvaluators(getter, rule) + types := evalTypes(evals) + assert.Equal(t, []string{evaluator.RuleTypeVersionCooldown}, types) + }) + + t.Run("union of all scopes covers all four evaluator types", func(t *testing.T) { + envEvals := EnvironmentRuleEvaluators(rule) + envVerEvals := EnvironmentVersionRuleEvaluators(getter, rule) + depVerEvals := DeploymentVersionRuleEvaluators(getter, rule) + + all := append(append(envEvals, envVerEvals...), depVerEvals...) + types := evalTypes(all) + assert.Contains(t, types, evaluator.RuleTypeDeploymentWindow) + assert.Contains(t, types, evaluator.RuleTypeApproval) + assert.Contains(t, types, evaluator.RuleTypeEnvironmentProgression) + assert.Contains(t, types, evaluator.RuleTypeVersionCooldown) + assert.Len(t, types, 4) + }) +} diff --git a/packages/db/drizzle/0162_same_kronos.sql b/packages/db/drizzle/0162_same_kronos.sql new file mode 100644 index 000000000..724d712f4 --- /dev/null +++ b/packages/db/drizzle/0162_same_kronos.sql @@ -0,0 +1,22 @@ +CREATE TABLE "policy_rule_summary" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "rule_id" uuid NOT NULL, + "deployment_id" uuid, + "environment_id" uuid, + "version_id" uuid, + "allowed" boolean NOT NULL, + "action_required" boolean DEFAULT false NOT NULL, + "action_type" text, + "message" text NOT NULL, + "details" jsonb DEFAULT '{}' NOT NULL, + "satisfied_at" timestamp with time zone, + "next_evaluation_at" timestamp with time zone, + "evaluated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "policy_rule_summary" ADD CONSTRAINT "policy_rule_summary_deployment_id_deployment_id_fk" FOREIGN KEY ("deployment_id") REFERENCES "public"."deployment"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "policy_rule_summary" ADD CONSTRAINT "policy_rule_summary_environment_id_environment_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environment"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "policy_rule_summary" ADD CONSTRAINT "policy_rule_summary_version_id_deployment_version_id_fk" FOREIGN KEY ("version_id") REFERENCES "public"."deployment_version"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "policy_rule_summary_rule_id_deployment_id_environment_id_version_id_index" ON "policy_rule_summary" USING btree ("rule_id","deployment_id","environment_id","version_id");--> statement-breakpoint +CREATE INDEX "policy_rule_summary_deployment_id_version_id_index" ON "policy_rule_summary" USING btree ("deployment_id","version_id");--> statement-breakpoint +CREATE INDEX "policy_rule_summary_environment_id_index" ON "policy_rule_summary" USING btree ("environment_id"); \ No newline at end of file diff --git a/packages/db/drizzle/meta/0162_snapshot.json b/packages/db/drizzle/meta/0162_snapshot.json new file mode 100644 index 000000000..cfc01de5a --- /dev/null +++ b/packages/db/drizzle/meta/0162_snapshot.json @@ -0,0 +1,5843 @@ +{ + "id": "3df5913d-902e-428c-a708-251431c2beec", + "prevId": "b0a123af-2a56-4139-9a3f-bc2d37bbb11b", + "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_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.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 + }, + "job_agent_id": { + "name": "job_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "job_agent_config": { + "name": "job_agent_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "job_agents": { + "name": "job_agents", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "resource_selector": { + "name": "resource_selector", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'false'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "deployment_workspace_id_index": { + "name": "deployment_workspace_id_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": {}, + "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": false + } + }, + "indexes": { + "environment_workspace_id_index": { + "name": "environment_workspace_id_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": {}, + "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_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": {}, + "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": {} + } + }, + "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_id_job_id_index": { + "name": "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": 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_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_payload": { + "name": "reconcile_work_payload", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "reconcile_work_payload_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "scope_ref": { + "name": "scope_ref", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "payload_type": { + "name": "payload_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "payload_key": { + "name": "payload_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "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": { + "reconcile_work_payload_scope_ref_payload_type_payload_key_index": { + "name": "reconcile_work_payload_scope_ref_payload_type_payload_key_index", + "columns": [ + { + "expression": "scope_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "payload_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "payload_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reconcile_work_payload_scope_ref_index": { + "name": "reconcile_work_payload_scope_ref_index", + "columns": [ + { + "expression": "scope_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reconcile_work_payload_scope_ref_reconcile_work_scope_id_fk": { + "name": "reconcile_work_payload_scope_ref_reconcile_work_scope_id_fk", + "tableFrom": "reconcile_work_payload", + "tableTo": "reconcile_work_scope", + "columnsFrom": [ + "scope_ref" + ], + "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()" + }, + "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_kind_not_before_priority_event_ts_claimed_until_index": { + "name": "reconcile_work_scope_kind_not_before_priority_event_ts_claimed_until_index", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "not_before", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_ts", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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 + }, + "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_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": "'[]'" + }, + "jobs": { + "name": "jobs", + "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 + }, + "ref": { + "name": "ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_job_template": { + "name": "workflow_job_template", + "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 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ref": { + "name": "ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "if_condition": { + "name": "if_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "matrix": { + "name": "matrix", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_job_template_workflow_id_workflow_id_fk": { + "name": "workflow_job_template_workflow_id_workflow_id_fk", + "tableFrom": "workflow_job_template", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_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.policy_rule_summary": { + "name": "policy_rule_summary", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "rule_id": { + "name": "rule_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "version_id": { + "name": "version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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_summary_rule_id_deployment_id_environment_id_version_id_index": { + "name": "policy_rule_summary_rule_id_deployment_id_environment_id_version_id_index", + "columns": [ + { + "expression": "rule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_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" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "policy_rule_summary_deployment_id_version_id_index": { + "name": "policy_rule_summary_deployment_id_version_id_index", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "policy_rule_summary_environment_id_index": { + "name": "policy_rule_summary_environment_id_index", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "policy_rule_summary_deployment_id_deployment_id_fk": { + "name": "policy_rule_summary_deployment_id_deployment_id_fk", + "tableFrom": "policy_rule_summary", + "tableTo": "deployment", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "policy_rule_summary_environment_id_environment_id_fk": { + "name": "policy_rule_summary_environment_id_environment_id_fk", + "tableFrom": "policy_rule_summary", + "tableTo": "environment", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "policy_rule_summary_version_id_deployment_version_id_fk": { + "name": "policy_rule_summary_version_id_deployment_version_id_fk", + "tableFrom": "policy_rule_summary", + "tableTo": "deployment_version", + "columnsFrom": [ + "version_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 + }, + "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": {} + } + }, + "foreignKeys": {}, + "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": {}, + "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 + } + }, + "enums": { + "public.system_role": { + "name": "system_role", + "schema": "public", + "values": [ + "user", + "admin" + ] + }, + "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" + ] + }, + "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" + ] + } + }, + "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 3b6b07b04..32328643b 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -1135,6 +1135,13 @@ "when": 1772693395056, "tag": "0161_ambitious_living_mummy", "breakpoints": true + }, + { + "idx": 162, + "version": "7", + "when": 1772850815650, + "tag": "0162_same_kronos", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/packages/db/src/schema/policy-rule-summary.ts b/packages/db/src/schema/policy-rule-summary.ts index 59077ff20..a49d12eee 100644 --- a/packages/db/src/schema/policy-rule-summary.ts +++ b/packages/db/src/schema/policy-rule-summary.ts @@ -13,15 +13,11 @@ import { import { deployment } from "./deployment.js"; import { deploymentVersion } from "./deployment-version.js"; import { environment } from "./environment.js"; -import { workspace } from "./workspace.js"; export const policyRuleSummary = pgTable( "policy_rule_summary", { id: uuid("id").primaryKey().defaultRandom(), - workspaceId: uuid("workspace_id") - .notNull() - .references(() => workspace.id, { onDelete: "cascade" }), ruleId: uuid("rule_id").notNull(), deploymentId: uuid("deployment_id").references(() => deployment.id, { @@ -50,7 +46,6 @@ export const policyRuleSummary = pgTable( uniqueIndex().on(t.ruleId, t.deploymentId, t.environmentId, t.versionId), index().on(t.deploymentId, t.versionId), index().on(t.environmentId), - index().on(t.workspaceId), ], ); From 456497e49785ed650b89db67cdbcc6b0cec57525 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Fri, 6 Mar 2026 19:18:13 -0800 Subject: [PATCH 04/11] fmt --- apps/workspace-engine/main.go | 2 +- .../pkg/workspace/releasemanager/policy/evaluators.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/workspace-engine/main.go b/apps/workspace-engine/main.go index a2149ba38..76b36cd6b 100644 --- a/apps/workspace-engine/main.go +++ b/apps/workspace-engine/main.go @@ -11,10 +11,10 @@ import ( "workspace-engine/svc" "workspace-engine/svc/controllers/deploymentresourceselectoreval" "workspace-engine/svc/controllers/desiredrelease" - "workspace-engine/svc/controllers/policysummary" "workspace-engine/svc/controllers/environmentresourceselectoreval" "workspace-engine/svc/controllers/jobdispatch" "workspace-engine/svc/controllers/jobverificationmetric" + "workspace-engine/svc/controllers/policysummary" "workspace-engine/svc/controllers/relationshipeval" httpsvc "workspace-engine/svc/http" "workspace-engine/svc/routerregistrar" diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluators.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluators.go index 4de54c8bd..db2179fe6 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluators.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluators.go @@ -36,4 +36,3 @@ func EvaluatorsForSummary(store *store.Store, rule *oapi.PolicyRule) []evaluator versioncooldown.NewSummaryEvaluatorFromStore(store, rule), ) } - From 7c7debbb40967491b8b93aad3ac4c97c981f2171 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Fri, 6 Mar 2026 19:58:51 -0800 Subject: [PATCH 05/11] single event --- apps/workspace-engine/pkg/db/batch.go | 14 +- apps/workspace-engine/pkg/db/models.go | 1 - .../pkg/db/policy_rule_summary.sql.go | 96 +------ .../pkg/db/queries/policy_rule_summary.sql | 32 +-- .../pkg/db/queries/schema.sql | 7 +- .../deploymentversion/deploymentversion.go | 35 --- .../events/handler/environment/environment.go | 14 - .../pkg/events/handler/jobs/jobs.go | 22 -- .../pkg/events/handler/policies/policies.go | 26 -- .../userapprovalrecords.go | 17 -- .../pkg/reconcile/events/policysummary.go | 61 +--- .../controller.go | 22 -- .../controllers/policysummary/controller.go | 2 +- .../controllers/policysummary/reconcile.go | 152 ++-------- .../svc/controllers/policysummary/scope.go | 41 +-- .../svc/controllers/policysummary/setters.go | 6 +- .../policysummary/setters_postgres.go | 13 +- .../policysummary/summaryeval/summaryeval.go | 15 +- .../summaryeval/summaryeval_test.go | 266 ++---------------- ...ame_kronos.sql => 0162_bizarre_jackal.sql} | 11 +- packages/db/drizzle/meta/0162_snapshot.json | 56 +--- packages/db/drizzle/meta/_journal.json | 4 +- packages/db/src/reconcilers/index.ts | 37 +++ packages/db/src/schema/policy-rule-summary.ts | 25 +- packages/trpc/src/routes/reconcile.ts | 11 + 25 files changed, 153 insertions(+), 833 deletions(-) rename packages/db/drizzle/{0162_same_kronos.sql => 0162_bizarre_jackal.sql} (54%) diff --git a/apps/workspace-engine/pkg/db/batch.go b/apps/workspace-engine/pkg/db/batch.go index 7817792d6..17871f922 100644 --- a/apps/workspace-engine/pkg/db/batch.go +++ b/apps/workspace-engine/pkg/db/batch.go @@ -209,19 +209,19 @@ func (b *UpsertChangelogEntryBatchResults) Close() error { const upsertPolicyRuleSummary = `-- name: UpsertPolicyRuleSummary :batchexec INSERT INTO policy_rule_summary ( id, rule_id, - deployment_id, environment_id, version_id, + environment_id, version_id, allowed, action_required, action_type, message, details, satisfied_at, next_evaluation_at, evaluated_at ) VALUES ( gen_random_uuid(), $1, - $2, $3, $4, - $5, $6, $7, - $8, $9, - $10, $11, NOW() + $2, $3, + $4, $5, $6, + $7, $8, + $9, $10, NOW() ) -ON CONFLICT (rule_id, deployment_id, environment_id, version_id) DO UPDATE +ON CONFLICT (rule_id, environment_id, version_id) DO UPDATE SET allowed = EXCLUDED.allowed, action_required = EXCLUDED.action_required, action_type = EXCLUDED.action_type, @@ -240,7 +240,6 @@ type UpsertPolicyRuleSummaryBatchResults struct { type UpsertPolicyRuleSummaryParams struct { RuleID uuid.UUID - DeploymentID uuid.UUID EnvironmentID uuid.UUID VersionID uuid.UUID Allowed bool @@ -257,7 +256,6 @@ func (q *Queries) UpsertPolicyRuleSummary(ctx context.Context, arg []UpsertPolic for _, a := range arg { vals := []interface{}{ a.RuleID, - a.DeploymentID, a.EnvironmentID, a.VersionID, a.Allowed, diff --git a/apps/workspace-engine/pkg/db/models.go b/apps/workspace-engine/pkg/db/models.go index d04d3b5a6..60b4a0ac2 100644 --- a/apps/workspace-engine/pkg/db/models.go +++ b/apps/workspace-engine/pkg/db/models.go @@ -467,7 +467,6 @@ type PolicyRuleRollback struct { type PolicyRuleSummary struct { ID uuid.UUID RuleID uuid.UUID - DeploymentID uuid.UUID EnvironmentID uuid.UUID VersionID uuid.UUID Allowed bool diff --git a/apps/workspace-engine/pkg/db/policy_rule_summary.sql.go b/apps/workspace-engine/pkg/db/policy_rule_summary.sql.go index d4efad227..c34053d13 100644 --- a/apps/workspace-engine/pkg/db/policy_rule_summary.sql.go +++ b/apps/workspace-engine/pkg/db/policy_rule_summary.sql.go @@ -20,102 +20,9 @@ func (q *Queries) DeletePolicyRuleSummariesByRuleID(ctx context.Context, ruleID return err } -const listPolicyRuleSummariesByDeploymentAndVersion = `-- name: ListPolicyRuleSummariesByDeploymentAndVersion :many -SELECT id, rule_id, - deployment_id, environment_id, version_id, - allowed, action_required, action_type, - message, details, - satisfied_at, next_evaluation_at, evaluated_at -FROM policy_rule_summary -WHERE deployment_id = $1 AND version_id = $2 -` - -type ListPolicyRuleSummariesByDeploymentAndVersionParams struct { - DeploymentID uuid.UUID - VersionID uuid.UUID -} - -func (q *Queries) ListPolicyRuleSummariesByDeploymentAndVersion(ctx context.Context, arg ListPolicyRuleSummariesByDeploymentAndVersionParams) ([]PolicyRuleSummary, error) { - rows, err := q.db.Query(ctx, listPolicyRuleSummariesByDeploymentAndVersion, arg.DeploymentID, arg.VersionID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []PolicyRuleSummary - for rows.Next() { - var i PolicyRuleSummary - if err := rows.Scan( - &i.ID, - &i.RuleID, - &i.DeploymentID, - &i.EnvironmentID, - &i.VersionID, - &i.Allowed, - &i.ActionRequired, - &i.ActionType, - &i.Message, - &i.Details, - &i.SatisfiedAt, - &i.NextEvaluationAt, - &i.EvaluatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const listPolicyRuleSummariesByEnvironment = `-- name: ListPolicyRuleSummariesByEnvironment :many -SELECT id, rule_id, - deployment_id, environment_id, version_id, - allowed, action_required, action_type, - message, details, - satisfied_at, next_evaluation_at, evaluated_at -FROM policy_rule_summary -WHERE environment_id = $1 -` - -func (q *Queries) ListPolicyRuleSummariesByEnvironment(ctx context.Context, environmentID uuid.UUID) ([]PolicyRuleSummary, error) { - rows, err := q.db.Query(ctx, listPolicyRuleSummariesByEnvironment, environmentID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []PolicyRuleSummary - for rows.Next() { - var i PolicyRuleSummary - if err := rows.Scan( - &i.ID, - &i.RuleID, - &i.DeploymentID, - &i.EnvironmentID, - &i.VersionID, - &i.Allowed, - &i.ActionRequired, - &i.ActionType, - &i.Message, - &i.Details, - &i.SatisfiedAt, - &i.NextEvaluationAt, - &i.EvaluatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - const listPolicyRuleSummariesByEnvironmentAndVersion = `-- name: ListPolicyRuleSummariesByEnvironmentAndVersion :many SELECT id, rule_id, - deployment_id, environment_id, version_id, + environment_id, version_id, allowed, action_required, action_type, message, details, satisfied_at, next_evaluation_at, evaluated_at @@ -140,7 +47,6 @@ func (q *Queries) ListPolicyRuleSummariesByEnvironmentAndVersion(ctx context.Con if err := rows.Scan( &i.ID, &i.RuleID, - &i.DeploymentID, &i.EnvironmentID, &i.VersionID, &i.Allowed, diff --git a/apps/workspace-engine/pkg/db/queries/policy_rule_summary.sql b/apps/workspace-engine/pkg/db/queries/policy_rule_summary.sql index 2829d29b2..a01b4b05b 100644 --- a/apps/workspace-engine/pkg/db/queries/policy_rule_summary.sql +++ b/apps/workspace-engine/pkg/db/queries/policy_rule_summary.sql @@ -1,19 +1,19 @@ -- name: UpsertPolicyRuleSummary :batchexec INSERT INTO policy_rule_summary ( id, rule_id, - deployment_id, environment_id, version_id, + environment_id, version_id, allowed, action_required, action_type, message, details, satisfied_at, next_evaluation_at, evaluated_at ) VALUES ( gen_random_uuid(), $1, - $2, $3, $4, - $5, $6, $7, - $8, $9, - $10, $11, NOW() + $2, $3, + $4, $5, $6, + $7, $8, + $9, $10, NOW() ) -ON CONFLICT (rule_id, deployment_id, environment_id, version_id) DO UPDATE +ON CONFLICT (rule_id, environment_id, version_id) DO UPDATE SET allowed = EXCLUDED.allowed, action_required = EXCLUDED.action_required, action_type = EXCLUDED.action_type, @@ -23,27 +23,9 @@ SET allowed = EXCLUDED.allowed, next_evaluation_at = EXCLUDED.next_evaluation_at, evaluated_at = NOW(); --- name: ListPolicyRuleSummariesByDeploymentAndVersion :many -SELECT id, rule_id, - deployment_id, environment_id, version_id, - allowed, action_required, action_type, - message, details, - satisfied_at, next_evaluation_at, evaluated_at -FROM policy_rule_summary -WHERE deployment_id = $1 AND version_id = $2; - --- name: ListPolicyRuleSummariesByEnvironment :many -SELECT id, rule_id, - deployment_id, environment_id, version_id, - allowed, action_required, action_type, - message, details, - satisfied_at, next_evaluation_at, evaluated_at -FROM policy_rule_summary -WHERE environment_id = $1; - -- name: ListPolicyRuleSummariesByEnvironmentAndVersion :many SELECT id, rule_id, - deployment_id, environment_id, version_id, + environment_id, version_id, allowed, action_required, action_type, message, details, satisfied_at, next_evaluation_at, evaluated_at diff --git a/apps/workspace-engine/pkg/db/queries/schema.sql b/apps/workspace-engine/pkg/db/queries/schema.sql index 2e49ccb1b..6c856675d 100644 --- a/apps/workspace-engine/pkg/db/queries/schema.sql +++ b/apps/workspace-engine/pkg/db/queries/schema.sql @@ -429,9 +429,8 @@ CREATE TABLE job_verification_metric_measurement ( CREATE TABLE policy_rule_summary ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), rule_id UUID NOT NULL, - deployment_id UUID, - environment_id UUID, - version_id UUID, + environment_id UUID NOT NULL, + version_id UUID NOT NULL, allowed BOOLEAN NOT NULL, action_required BOOLEAN NOT NULL DEFAULT false, action_type TEXT, @@ -443,4 +442,4 @@ CREATE TABLE policy_rule_summary ( ); CREATE UNIQUE INDEX policy_rule_summary_scope_idx - ON policy_rule_summary (rule_id, deployment_id, environment_id, version_id); + ON policy_rule_summary (rule_id, environment_id, version_id); diff --git a/apps/workspace-engine/pkg/events/handler/deploymentversion/deploymentversion.go b/apps/workspace-engine/pkg/events/handler/deploymentversion/deploymentversion.go index b0576f356..06a7e70c0 100644 --- a/apps/workspace-engine/pkg/events/handler/deploymentversion/deploymentversion.go +++ b/apps/workspace-engine/pkg/events/handler/deploymentversion/deploymentversion.go @@ -6,45 +6,16 @@ import ( "workspace-engine/pkg/events/handler" "workspace-engine/pkg/oapi" - "workspace-engine/pkg/reconcile/events" "workspace-engine/pkg/workspace" "workspace-engine/pkg/workspace/releasemanager" "workspace-engine/pkg/workspace/releasemanager/trace" - "github.com/charmbracelet/log" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" ) var tracer = otel.Tracer("events/handler/deploymentversion") -func enqueuePolicySummaries(ctx context.Context, ws *workspace.Workspace, version *oapi.DeploymentVersion, releaseTargets []*oapi.ReleaseTarget) { - seen := make(map[string]struct{}) - var params []events.PolicySummaryParams - - for _, rt := range releaseTargets { - evKey := rt.EnvironmentId + ":" + version.Id - if _, ok := seen[evKey]; !ok { - seen[evKey] = struct{}{} - params = append(params, events.EnvironmentVersionSummaryParams{ - WorkspaceID: ws.ID, - EnvironmentID: rt.EnvironmentId, - VersionID: version.Id, - }.ToParams()) - } - } - - params = append(params, events.DeploymentVersionSummaryParams{ - WorkspaceID: ws.ID, - DeploymentID: version.DeploymentId, - VersionID: version.Id, - }.ToParams()) - - if err := events.EnqueueManyPolicySummary(ws.Queue(), ctx, params); err != nil { - log.Error("failed to enqueue policy summaries for version change", "error", err) - } -} - func HandleDeploymentVersionCreated( ctx context.Context, ws *workspace.Workspace, @@ -76,8 +47,6 @@ func HandleDeploymentVersionCreated( _ = ws.ReleaseManager().ReconcileTargets(ctx, releaseTargets, releasemanager.WithTrigger(trace.TriggerVersionCreated)) - enqueuePolicySummaries(ctx, ws, deploymentVersion, releaseTargets) - return nil } @@ -111,8 +80,6 @@ func HandleDeploymentVersionUpdated( _ = ws.ReleaseManager().ReconcileTargets(ctx, releaseTargets, releasemanager.WithTrigger(trace.TriggerVersionCreated)) - enqueuePolicySummaries(ctx, ws, deploymentVersion, releaseTargets) - return nil } @@ -147,7 +114,5 @@ func HandleDeploymentVersionDeleted( _ = ws.ReleaseManager().ReconcileTargets(ctx, releaseTargets, releasemanager.WithTrigger(trace.TriggerVersionCreated)) - enqueuePolicySummaries(ctx, ws, deploymentVersion, releaseTargets) - return nil } diff --git a/apps/workspace-engine/pkg/events/handler/environment/environment.go b/apps/workspace-engine/pkg/events/handler/environment/environment.go index e1a3e6148..6d9f086c1 100644 --- a/apps/workspace-engine/pkg/events/handler/environment/environment.go +++ b/apps/workspace-engine/pkg/events/handler/environment/environment.go @@ -86,13 +86,6 @@ func HandleEnvironmentCreated( releasemanager.WithTrigger(trace.TriggerEnvironmentCreated)) } - if err := events.EnqueuePolicySummary(ws.Queue(), ctx, events.EnvironmentSummaryParams{ - WorkspaceID: ws.ID, - EnvironmentID: environment.Id, - }.ToParams()); err != nil { - log.Error("failed to enqueue policy summary for environment created", "error", err) - } - return nil } @@ -188,13 +181,6 @@ func HandleEnvironmentUpdated( _ = ws.ReleaseManager().ReconcileTargets(ctx, reconileReleaseTargets, releasemanager.WithTrigger(trace.TriggerEnvironmentUpdated)) - if err := events.EnqueuePolicySummary(ws.Queue(), ctx, events.EnvironmentSummaryParams{ - WorkspaceID: ws.ID, - EnvironmentID: environment.Id, - }.ToParams()); err != nil { - log.Error("failed to enqueue policy summary for environment updated", "error", err) - } - return nil } diff --git a/apps/workspace-engine/pkg/events/handler/jobs/jobs.go b/apps/workspace-engine/pkg/events/handler/jobs/jobs.go index 9849b1924..6d7c23cdc 100644 --- a/apps/workspace-engine/pkg/events/handler/jobs/jobs.go +++ b/apps/workspace-engine/pkg/events/handler/jobs/jobs.go @@ -6,7 +6,6 @@ import ( "workspace-engine/pkg/events/handler" "workspace-engine/pkg/oapi" - "workspace-engine/pkg/reconcile/events" "workspace-engine/pkg/workspace" "github.com/charmbracelet/log" @@ -42,9 +41,6 @@ func HandleJobUpdated( ws.Jobs().Upsert(ctx, &jobUpdateEvent.Job) dirtyStateForJob(ctx, ws, &jobUpdateEvent.Job) triggerActionsOnStatusChange(ctx, ws, &jobUpdateEvent.Job, previousStatus) - if jobUpdateEvent.Job.Status != previousStatus { - enqueuePolicySummaryForJob(ctx, ws, &jobUpdateEvent.Job) - } return nil } @@ -63,10 +59,6 @@ func HandleJobUpdated( // Trigger actions on status change triggerActionsOnStatusChange(ctx, ws, mergedJob, previousStatus) - if mergedJob.Status != previousStatus { - enqueuePolicySummaryForJob(ctx, ws, mergedJob) - } - go func() { if err := MaybeAddCommitStatusFromJob(ws, mergedJob); err != nil { log.Error("error adding commit status", "error", err.Error()) @@ -115,20 +107,6 @@ func dirtyStateForJob(ctx context.Context, ws *workspace.Workspace, job *oapi.Jo ws.ReleaseManager().RecomputeState(ctx) } -func enqueuePolicySummaryForJob(ctx context.Context, ws *workspace.Workspace, job *oapi.Job) { - release, exists := ws.Releases().Get(job.ReleaseId) - if !exists { - return - } - if err := events.EnqueuePolicySummary(ws.Queue(), ctx, events.EnvironmentVersionSummaryParams{ - WorkspaceID: ws.ID, - EnvironmentID: release.ReleaseTarget.EnvironmentId, - VersionID: release.Version.Id, - }.ToParams()); err != nil { - log.Error("failed to enqueue policy summary for job update", "error", err) - } -} - func getJob(ws *workspace.Workspace, job *oapi.JobUpdateEvent) (*oapi.Job, bool) { if job.Id != nil && *job.Id != "" { if existing, exists := ws.Jobs().Get(*job.Id); exists { diff --git a/apps/workspace-engine/pkg/events/handler/policies/policies.go b/apps/workspace-engine/pkg/events/handler/policies/policies.go index 696cd7f3b..9512b26c5 100644 --- a/apps/workspace-engine/pkg/events/handler/policies/policies.go +++ b/apps/workspace-engine/pkg/events/handler/policies/policies.go @@ -6,7 +6,6 @@ import ( "workspace-engine/pkg/events/handler" "workspace-engine/pkg/oapi" - "workspace-engine/pkg/reconcile/events" "workspace-engine/pkg/workspace" "workspace-engine/pkg/workspace/releasemanager" "workspace-engine/pkg/workspace/releasemanager/trace" @@ -46,25 +45,6 @@ func getAffectedTargets(ctx context.Context, ws *workspace.Workspace, policyID s return affectedTargets } -func enqueuePolicySummariesForTargets(ctx context.Context, ws *workspace.Workspace, targets []*oapi.ReleaseTarget) { - envSeen := make(map[string]struct{}) - var params []events.PolicySummaryParams - - for _, rt := range targets { - if _, ok := envSeen[rt.EnvironmentId]; !ok { - envSeen[rt.EnvironmentId] = struct{}{} - params = append(params, events.EnvironmentSummaryParams{ - WorkspaceID: ws.ID, - EnvironmentID: rt.EnvironmentId, - }.ToParams()) - } - } - - if err := events.EnqueueManyPolicySummary(ws.Queue(), ctx, params); err != nil { - log.Error("failed to enqueue policy summaries for policy change", "error", err) - } -} - func HandlePolicyCreated( ctx context.Context, ws *workspace.Workspace, @@ -90,8 +70,6 @@ func HandlePolicyCreated( _ = ws.ReleaseManager().ReconcileTargets(ctx, affectedTargets, releasemanager.WithTrigger(trace.TriggerPolicyUpdated)) - enqueuePolicySummariesForTargets(ctx, ws, affectedTargets) - return nil } @@ -141,8 +119,6 @@ func HandlePolicyUpdated( _ = ws.ReleaseManager().ReconcileTargets(ctx, affectedTargets, releasemanager.WithTrigger(trace.TriggerPolicyUpdated)) - enqueuePolicySummariesForTargets(ctx, ws, affectedTargets) - return nil } @@ -168,7 +144,5 @@ func HandlePolicyDeleted( _ = ws.ReleaseManager().ReconcileTargets(ctx, affectedTargets, releasemanager.WithTrigger(trace.TriggerPolicyUpdated)) - enqueuePolicySummariesForTargets(ctx, ws, affectedTargets) - return nil } diff --git a/apps/workspace-engine/pkg/events/handler/userapprovalrecords/userapprovalrecords.go b/apps/workspace-engine/pkg/events/handler/userapprovalrecords/userapprovalrecords.go index 26b047075..94be0a831 100644 --- a/apps/workspace-engine/pkg/events/handler/userapprovalrecords/userapprovalrecords.go +++ b/apps/workspace-engine/pkg/events/handler/userapprovalrecords/userapprovalrecords.go @@ -7,7 +7,6 @@ import ( "workspace-engine/pkg/events/handler" "workspace-engine/pkg/oapi" - "workspace-engine/pkg/reconcile/events" "workspace-engine/pkg/workspace" "workspace-engine/pkg/workspace/releasemanager" "workspace-engine/pkg/workspace/releasemanager/trace" @@ -51,16 +50,6 @@ func getRelevantTargets(ctx context.Context, ws *workspace.Workspace, userApprov return releaseTargets, nil } -func enqueuePolicySummary(ctx context.Context, ws *workspace.Workspace, record *oapi.UserApprovalRecord) { - if err := events.EnqueuePolicySummary(ws.Queue(), ctx, events.EnvironmentVersionSummaryParams{ - WorkspaceID: ws.ID, - EnvironmentID: record.EnvironmentId, - VersionID: record.VersionId, - }.ToParams()); err != nil { - log.Error("failed to enqueue policy summary for approval", "error", err) - } -} - func HandleUserApprovalRecordCreated( ctx context.Context, ws *workspace.Workspace, @@ -98,8 +87,6 @@ func HandleUserApprovalRecordCreated( _ = ws.ReleaseManager().ReconcileTargets(ctx, relevantTargets, releasemanager.WithTrigger(trace.TriggerApprovalCreated)) - enqueuePolicySummary(ctx, ws, userApprovalRecord) - return nil } @@ -135,8 +122,6 @@ func HandleUserApprovalRecordUpdated( _ = ws.ReleaseManager().ReconcileTargets(ctx, relevantTargets, releasemanager.WithTrigger(trace.TriggerApprovalUpdated)) - enqueuePolicySummary(ctx, ws, userApprovalRecord) - return nil } @@ -172,7 +157,5 @@ func HandleUserApprovalRecordDeleted( _ = ws.ReleaseManager().ReconcileTargets(ctx, relevantTargets, releasemanager.WithTrigger(trace.TriggerApprovalUpdated)) - enqueuePolicySummary(ctx, ws, userApprovalRecord) - return nil } diff --git a/apps/workspace-engine/pkg/reconcile/events/policysummary.go b/apps/workspace-engine/pkg/reconcile/events/policysummary.go index 3ccbb743b..0be0fa024 100644 --- a/apps/workspace-engine/pkg/reconcile/events/policysummary.go +++ b/apps/workspace-engine/pkg/reconcile/events/policysummary.go @@ -10,73 +10,22 @@ import ( const PolicySummaryKind = "policy-summary" -const ( - PolicySummaryScopeEnvironment = "environment" - PolicySummaryScopeEnvironmentVersion = "environment-version" - PolicySummaryScopeDeploymentVersion = "deployment-version" -) - type PolicySummaryParams struct { - WorkspaceID string - ScopeType string - ScopeID string -} - -type EnvironmentSummaryParams struct { - WorkspaceID string - EnvironmentID string -} - -func (p EnvironmentSummaryParams) ToParams() PolicySummaryParams { - return PolicySummaryParams{ - WorkspaceID: p.WorkspaceID, - ScopeType: PolicySummaryScopeEnvironment, - ScopeID: p.EnvironmentID, - } -} - -type EnvironmentVersionSummaryParams struct { WorkspaceID string EnvironmentID string VersionID string } -func (p EnvironmentVersionSummaryParams) ScopeID() string { +func (p PolicySummaryParams) ScopeID() string { return fmt.Sprintf("%s:%s", p.EnvironmentID, p.VersionID) } -func (p EnvironmentVersionSummaryParams) ToParams() PolicySummaryParams { - return PolicySummaryParams{ - WorkspaceID: p.WorkspaceID, - ScopeType: PolicySummaryScopeEnvironmentVersion, - ScopeID: p.ScopeID(), - } -} - -type DeploymentVersionSummaryParams struct { - WorkspaceID string - DeploymentID string - VersionID string -} - -func (p DeploymentVersionSummaryParams) ScopeID() string { - return fmt.Sprintf("%s:%s", p.DeploymentID, p.VersionID) -} - -func (p DeploymentVersionSummaryParams) ToParams() PolicySummaryParams { - return PolicySummaryParams{ - WorkspaceID: p.WorkspaceID, - ScopeType: PolicySummaryScopeDeploymentVersion, - ScopeID: p.ScopeID(), - } -} - func EnqueuePolicySummary(queue reconcile.Queue, ctx context.Context, params PolicySummaryParams) error { return queue.Enqueue(ctx, reconcile.EnqueueParams{ WorkspaceID: params.WorkspaceID, Kind: PolicySummaryKind, - ScopeType: params.ScopeType, - ScopeID: params.ScopeID, + ScopeType: "environment-version", + ScopeID: params.ScopeID(), }) } @@ -90,8 +39,8 @@ func EnqueueManyPolicySummary(queue reconcile.Queue, ctx context.Context, params items[i] = reconcile.EnqueueParams{ WorkspaceID: p.WorkspaceID, Kind: PolicySummaryKind, - ScopeType: p.ScopeType, - ScopeID: p.ScopeID, + ScopeType: "environment-version", + ScopeID: p.ScopeID(), } } return queue.EnqueueMany(ctx, items) diff --git a/apps/workspace-engine/svc/controllers/deploymentresourceselectoreval/controller.go b/apps/workspace-engine/svc/controllers/deploymentresourceselectoreval/controller.go index 81a86167d..d408de6f8 100644 --- a/apps/workspace-engine/svc/controllers/deploymentresourceselectoreval/controller.go +++ b/apps/workspace-engine/svc/controllers/deploymentresourceselectoreval/controller.go @@ -89,9 +89,6 @@ func (c *Controller) Process(ctx context.Context, item reconcile.Item) (reconcil if err := c.enqueueReleaseTargets(ctx, deployment.WorkspaceID, releaseTargets); err != nil { return reconcile.Result{}, fmt.Errorf("enqueue release targets: %w", err) } - if err := c.enqueuePolicySummaries(ctx, deployment.WorkspaceID, releaseTargets); err != nil { - log.Error("failed to enqueue policy summaries", "error", err) - } } return reconcile.Result{}, nil @@ -115,25 +112,6 @@ func (c *Controller) enqueueReleaseTargets(ctx context.Context, workspaceID uuid return events.EnqueueManyDesiredRelease(c.queue, ctx, params) } -func (c *Controller) enqueuePolicySummaries(ctx context.Context, workspaceID uuid.UUID, releaseTargets []ReleaseTarget) error { - wsID := workspaceID.String() - seen := make(map[string]struct{}) - var params []events.PolicySummaryParams - - for _, rt := range releaseTargets { - key := rt.EnvironmentID.String() - if _, ok := seen[key]; !ok { - seen[key] = struct{}{} - params = append(params, events.EnvironmentSummaryParams{ - WorkspaceID: wsID, - EnvironmentID: rt.EnvironmentID.String(), - }.ToParams()) - } - } - - return events.EnqueueManyPolicySummary(c.queue, ctx, params) -} - // evalResources streams resources from the DB and evaluates the CEL selector // concurrently, returning the IDs of all matched resources. func (c *Controller) evalResources(ctx context.Context, deployment *DeploymentInfo, selector cel.Program) ([]uuid.UUID, error) { diff --git a/apps/workspace-engine/svc/controllers/policysummary/controller.go b/apps/workspace-engine/svc/controllers/policysummary/controller.go index 1fac29c4e..42a3454d4 100644 --- a/apps/workspace-engine/svc/controllers/policysummary/controller.go +++ b/apps/workspace-engine/svc/controllers/policysummary/controller.go @@ -37,7 +37,7 @@ func (c *Controller) Process(ctx context.Context, item reconcile.Item) (reconcil attribute.String("item.scope_id", item.ScopeID), ) - result, err := Reconcile(ctx, item.WorkspaceID, item.ScopeType, item.ScopeID, c.getter, c.setter) + result, err := Reconcile(ctx, item.WorkspaceID, item.ScopeID, c.getter, c.setter) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) diff --git a/apps/workspace-engine/svc/controllers/policysummary/reconcile.go b/apps/workspace-engine/svc/controllers/policysummary/reconcile.go index ae6ca273f..df0004a60 100644 --- a/apps/workspace-engine/svc/controllers/policysummary/reconcile.go +++ b/apps/workspace-engine/svc/controllers/policysummary/reconcile.go @@ -5,8 +5,6 @@ import ( "fmt" "time" - "workspace-engine/pkg/oapi" - "workspace-engine/pkg/reconcile/events" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator" "workspace-engine/svc/controllers/policysummary/summaryeval" @@ -23,41 +21,8 @@ type reconciler struct { setter Setter } -func (r *reconciler) reconcileEnvironment(ctx context.Context, scope *EnvironmentScope) (*ReconcileResult, error) { - ctx, span := tracer.Start(ctx, "policysummary.reconcileEnvironment") - defer span.End() - - env, err := r.getter.GetEnvironment(ctx, scope.EnvironmentID.String()) - if err != nil { - return nil, fmt.Errorf("get environment: %w", err) - } - - evalScope := evaluator.EvaluatorScope{ - Environment: env, - } - - policies, err := r.getter.GetPoliciesForEnvironment(ctx, r.workspaceID, scope.EnvironmentID) - if err != nil { - return nil, fmt.Errorf("get policies: %w", err) - } - - rows, nextTime := r.evaluateAndCollect(ctx, policies, evalScope, func(rule *oapi.PolicyRule) []evaluator.Evaluator { - return summaryeval.EnvironmentRuleEvaluators(rule) - }) - - for i := range rows { - rows[i].EnvironmentID = &scope.EnvironmentID - } - - if err := r.setter.UpsertRuleSummaries(ctx, rows); err != nil { - return nil, fmt.Errorf("upsert rule summaries: %w", err) - } - - return &ReconcileResult{NextReconcileAt: nextTime}, nil -} - -func (r *reconciler) reconcileEnvironmentVersion(ctx context.Context, scope *EnvironmentVersionScope) (*ReconcileResult, error) { - ctx, span := tracer.Start(ctx, "policysummary.reconcileEnvironmentVersion") +func (r *reconciler) reconcile(ctx context.Context, scope *Scope) (*ReconcileResult, error) { + ctx, span := tracer.Start(ctx, "policysummary.reconcile") defer span.End() env, err := r.getter.GetEnvironment(ctx, scope.EnvironmentID.String()) @@ -80,86 +45,26 @@ func (r *reconciler) reconcileEnvironmentVersion(ctx context.Context, scope *Env return nil, fmt.Errorf("get policies: %w", err) } - rows, nextTime := r.evaluateAndCollect(ctx, policies, evalScope, func(rule *oapi.PolicyRule) []evaluator.Evaluator { - return summaryeval.EnvironmentVersionRuleEvaluators(r.getter, rule) - }) - - for i := range rows { - rows[i].EnvironmentID = &scope.EnvironmentID - rows[i].VersionID = &scope.VersionID - } - - if err := r.setter.UpsertRuleSummaries(ctx, rows); err != nil { - return nil, fmt.Errorf("upsert rule summaries: %w", err) - } - - return &ReconcileResult{NextReconcileAt: nextTime}, nil -} - -func (r *reconciler) reconcileDeploymentVersion(ctx context.Context, scope *DeploymentVersionScope) (*ReconcileResult, error) { - ctx, span := tracer.Start(ctx, "policysummary.reconcileDeploymentVersion") - defer span.End() - - deployment, err := r.getter.GetDeployment(ctx, scope.DeploymentID.String()) - if err != nil { - return nil, fmt.Errorf("get deployment: %w", err) - } - - version, err := r.getter.GetVersion(ctx, scope.VersionID) - if err != nil { - return nil, fmt.Errorf("get version: %w", err) - } - - evalScope := evaluator.EvaluatorScope{ - Deployment: deployment, - Version: version, - } - - policies, err := r.getter.GetPoliciesForDeployment(ctx, r.workspaceID, scope.DeploymentID) - if err != nil { - return nil, fmt.Errorf("get policies: %w", err) - } - - rows, nextTime := r.evaluateAndCollect(ctx, policies, evalScope, func(rule *oapi.PolicyRule) []evaluator.Evaluator { - return summaryeval.DeploymentVersionRuleEvaluators(r.getter, rule) - }) - - for i := range rows { - rows[i].DeploymentID = &scope.DeploymentID - rows[i].VersionID = &scope.VersionID - } - - if err := r.setter.UpsertRuleSummaries(ctx, rows); err != nil { - return nil, fmt.Errorf("upsert rule summaries: %w", err) - } - - return &ReconcileResult{NextReconcileAt: nextTime}, nil -} - -func (r *reconciler) evaluateAndCollect( - ctx context.Context, - policies []*oapi.Policy, - scope evaluator.EvaluatorScope, - evaluatorFactory func(rule *oapi.PolicyRule) []evaluator.Evaluator, -) ([]RuleSummaryRow, *time.Time) { var rows []RuleSummaryRow var nextTime *time.Time for _, p := range policies { for _, rule := range p.Rules { - evals := evaluatorFactory(&rule) + evals := summaryeval.RuleEvaluators(r.getter, &rule) for _, eval := range evals { if eval == nil { continue } - if !scope.HasFields(eval.ScopeFields()) { + if !evalScope.HasFields(eval.ScopeFields()) { continue } - result := eval.Evaluate(ctx, scope) + result := eval.Evaluate(ctx, evalScope) rows = append(rows, RuleSummaryRow{ - RuleID: uuid.MustParse(rule.Id), - Evaluation: result, + RuleID: uuid.MustParse(rule.Id), + EnvironmentID: scope.EnvironmentID, + VersionID: scope.VersionID, + Evaluation: result, }) if result.NextEvaluationTime != nil { @@ -171,39 +76,24 @@ func (r *reconciler) evaluateAndCollect( } } - return rows, nextTime + if err := r.setter.UpsertRuleSummaries(ctx, rows); err != nil { + return nil, fmt.Errorf("upsert rule summaries: %w", err) + } + + return &ReconcileResult{NextReconcileAt: nextTime}, nil } -func Reconcile(ctx context.Context, workspaceID string, scopeType string, scopeID string, getter Getter, setter Setter) (*ReconcileResult, error) { +func Reconcile(ctx context.Context, workspaceID string, scopeID string, getter Getter, setter Setter) (*ReconcileResult, error) { + scope, err := ParseScope(scopeID) + if err != nil { + return nil, err + } + r := &reconciler{ workspaceID: uuid.MustParse(workspaceID), getter: getter, setter: setter, } - switch scopeType { - case events.PolicySummaryScopeEnvironment: - scope, err := ParseEnvironmentScope(scopeID) - if err != nil { - return nil, err - } - return r.reconcileEnvironment(ctx, scope) - - case events.PolicySummaryScopeEnvironmentVersion: - scope, err := ParseEnvironmentVersionScope(scopeID) - if err != nil { - return nil, err - } - return r.reconcileEnvironmentVersion(ctx, scope) - - case events.PolicySummaryScopeDeploymentVersion: - scope, err := ParseDeploymentVersionScope(scopeID) - if err != nil { - return nil, err - } - return r.reconcileDeploymentVersion(ctx, scope) - - default: - return nil, fmt.Errorf("unknown policy summary scope type: %s", scopeType) - } + return r.reconcile(ctx, scope) } diff --git a/apps/workspace-engine/svc/controllers/policysummary/scope.go b/apps/workspace-engine/svc/controllers/policysummary/scope.go index 92e08d0e7..773d5d270 100644 --- a/apps/workspace-engine/svc/controllers/policysummary/scope.go +++ b/apps/workspace-engine/svc/controllers/policysummary/scope.go @@ -7,27 +7,15 @@ import ( "github.com/google/uuid" ) -type EnvironmentScope struct { - EnvironmentID uuid.UUID -} - -func ParseEnvironmentScope(scopeID string) (*EnvironmentScope, error) { - envID, err := uuid.Parse(scopeID) - if err != nil { - return nil, fmt.Errorf("parse environment scope: %w", err) - } - return &EnvironmentScope{EnvironmentID: envID}, nil -} - -type EnvironmentVersionScope struct { +type Scope struct { EnvironmentID uuid.UUID VersionID uuid.UUID } -func ParseEnvironmentVersionScope(scopeID string) (*EnvironmentVersionScope, error) { +func ParseScope(scopeID string) (*Scope, error) { parts := strings.SplitN(scopeID, ":", 2) if len(parts) != 2 { - return nil, fmt.Errorf("invalid environment-version scope: %s", scopeID) + return nil, fmt.Errorf("invalid policy summary scope: %s", scopeID) } envID, err := uuid.Parse(parts[0]) if err != nil { @@ -37,26 +25,5 @@ func ParseEnvironmentVersionScope(scopeID string) (*EnvironmentVersionScope, err if err != nil { return nil, fmt.Errorf("parse version id: %w", err) } - return &EnvironmentVersionScope{EnvironmentID: envID, VersionID: versionID}, nil -} - -type DeploymentVersionScope struct { - DeploymentID uuid.UUID - VersionID uuid.UUID -} - -func ParseDeploymentVersionScope(scopeID string) (*DeploymentVersionScope, error) { - parts := strings.SplitN(scopeID, ":", 2) - if len(parts) != 2 { - return nil, fmt.Errorf("invalid deployment-version scope: %s", scopeID) - } - depID, err := uuid.Parse(parts[0]) - if err != nil { - return nil, fmt.Errorf("parse deployment id: %w", err) - } - versionID, err := uuid.Parse(parts[1]) - if err != nil { - return nil, fmt.Errorf("parse version id: %w", err) - } - return &DeploymentVersionScope{DeploymentID: depID, VersionID: versionID}, nil + return &Scope{EnvironmentID: envID, VersionID: versionID}, nil } diff --git a/apps/workspace-engine/svc/controllers/policysummary/setters.go b/apps/workspace-engine/svc/controllers/policysummary/setters.go index 9640afde3..96d28e9f4 100644 --- a/apps/workspace-engine/svc/controllers/policysummary/setters.go +++ b/apps/workspace-engine/svc/controllers/policysummary/setters.go @@ -8,12 +8,10 @@ import ( "github.com/google/uuid" ) -// RuleSummaryRow is the DB representation of a single rule evaluation result. type RuleSummaryRow struct { RuleID uuid.UUID - DeploymentID *uuid.UUID - EnvironmentID *uuid.UUID - VersionID *uuid.UUID + EnvironmentID uuid.UUID + VersionID uuid.UUID Evaluation *oapi.RuleEvaluation } diff --git a/apps/workspace-engine/svc/controllers/policysummary/setters_postgres.go b/apps/workspace-engine/svc/controllers/policysummary/setters_postgres.go index be7ae8c57..cf0dcd096 100644 --- a/apps/workspace-engine/svc/controllers/policysummary/setters_postgres.go +++ b/apps/workspace-engine/svc/controllers/policysummary/setters_postgres.go @@ -6,7 +6,6 @@ import ( "workspace-engine/pkg/db" - "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" ) @@ -20,13 +19,6 @@ func NewPostgresSetter(queries *db.Queries) *PostgresSetter { return &PostgresSetter{queries: queries} } -func uuidOrZero(id *uuid.UUID) uuid.UUID { - if id != nil { - return *id - } - return uuid.Nil -} - func (s *PostgresSetter) UpsertRuleSummaries(ctx context.Context, rows []RuleSummaryRow) error { if len(rows) == 0 { return nil @@ -58,9 +50,8 @@ func (s *PostgresSetter) UpsertRuleSummaries(ctx context.Context, rows []RuleSum params[i] = db.UpsertPolicyRuleSummaryParams{ RuleID: row.RuleID, - DeploymentID: uuidOrZero(row.DeploymentID), - EnvironmentID: uuidOrZero(row.EnvironmentID), - VersionID: uuidOrZero(row.VersionID), + EnvironmentID: row.EnvironmentID, + VersionID: row.VersionID, Allowed: eval.Allowed, ActionRequired: eval.ActionRequired, ActionType: actionType, diff --git a/apps/workspace-engine/svc/controllers/policysummary/summaryeval/summaryeval.go b/apps/workspace-engine/svc/controllers/policysummary/summaryeval/summaryeval.go index a89d5214f..85f6b1f69 100644 --- a/apps/workspace-engine/svc/controllers/policysummary/summaryeval/summaryeval.go +++ b/apps/workspace-engine/svc/controllers/policysummary/summaryeval/summaryeval.go @@ -9,21 +9,14 @@ import ( "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versioncooldown" ) -func EnvironmentRuleEvaluators(rule *oapi.PolicyRule) []evaluator.Evaluator { +// RuleEvaluators returns all summary evaluators for a given policy rule. +func RuleEvaluators(getter Getter, rule *oapi.PolicyRule) []evaluator.Evaluator { return evaluator.CollectEvaluators( deploymentwindow.NewSummaryEvaluator(rule), - ) -} - -func EnvironmentVersionRuleEvaluators(getter Getter, rule *oapi.PolicyRule) []evaluator.Evaluator { - return evaluator.CollectEvaluators( approval.NewEvaluator(getter, rule), environmentprogression.NewEvaluator(getter, rule), - ) -} - -func DeploymentVersionRuleEvaluators(getter Getter, rule *oapi.PolicyRule) []evaluator.Evaluator { - return evaluator.CollectEvaluators( versioncooldown.NewSummaryEvaluator(getter, rule), + // TODO: add gradualrollout.NewSummaryEvaluator(getter, rule) + // once the getter-based constructor is added ) } diff --git a/apps/workspace-engine/svc/controllers/policysummary/summaryeval/summaryeval_test.go b/apps/workspace-engine/svc/controllers/policysummary/summaryeval/summaryeval_test.go index f48fa2053..6386c6fe2 100644 --- a/apps/workspace-engine/svc/controllers/policysummary/summaryeval/summaryeval_test.go +++ b/apps/workspace-engine/svc/controllers/policysummary/summaryeval/summaryeval_test.go @@ -118,282 +118,70 @@ func evalTypes(evals []evaluator.Evaluator) []string { } // --------------------------------------------------------------------------- -// EnvironmentRuleEvaluators tests +// RuleEvaluators tests // --------------------------------------------------------------------------- -func TestEnvironmentRuleEvaluators(t *testing.T) { - t.Run("returns empty for nil rule", func(t *testing.T) { - evals := EnvironmentRuleEvaluators(nil) - assert.Empty(t, evals) - }) - - t.Run("returns empty for rule without deployment window", func(t *testing.T) { - evals := EnvironmentRuleEvaluators(emptyRule("r-1")) - assert.Empty(t, evals) - }) - - t.Run("returns empty for rule with only approval", func(t *testing.T) { - evals := EnvironmentRuleEvaluators(ruleWithApproval("r-1")) - assert.Empty(t, evals) - }) - - t.Run("returns empty for rule with only version cooldown", func(t *testing.T) { - evals := EnvironmentRuleEvaluators(ruleWithVersionCooldown("r-1")) - assert.Empty(t, evals) - }) - - t.Run("returns empty for rule with only environment progression", func(t *testing.T) { - evals := EnvironmentRuleEvaluators(ruleWithEnvironmentProgression("r-1")) - assert.Empty(t, evals) - }) - - t.Run("returns deployment window evaluator", func(t *testing.T) { - evals := EnvironmentRuleEvaluators(ruleWithDeploymentWindow("r-1")) - require.Len(t, evals, 1) - assert.Equal(t, evaluator.RuleTypeDeploymentWindow, evals[0].RuleType()) - }) - - t.Run("deployment window evaluator has correct scope fields", func(t *testing.T) { - evals := EnvironmentRuleEvaluators(ruleWithDeploymentWindow("r-1")) - require.Len(t, evals, 1) - assert.NotZero(t, evals[0].ScopeFields()&evaluator.ScopeEnvironment) - }) - - t.Run("returns deployment window even when other rule types present", func(t *testing.T) { - rule := &oapi.PolicyRule{ - Id: "r-1", - AnyApproval: &oapi.AnyApprovalRule{MinApprovals: 2}, - DeploymentWindow: &oapi.DeploymentWindowRule{ - Rrule: "FREQ=DAILY;BYHOUR=10", - DurationMinutes: 120, - }, - VersionCooldown: &oapi.VersionCooldownRule{IntervalSeconds: 60}, - } - evals := EnvironmentRuleEvaluators(rule) - require.Len(t, evals, 1) - assert.Equal(t, evaluator.RuleTypeDeploymentWindow, evals[0].RuleType()) - }) - - t.Run("returns empty for deployment window with invalid rrule", func(t *testing.T) { - rule := &oapi.PolicyRule{ - Id: "r-1", - DeploymentWindow: &oapi.DeploymentWindowRule{ - Rrule: "INVALID-RRULE-STRING", - DurationMinutes: 60, - }, - } - evals := EnvironmentRuleEvaluators(rule) - assert.Empty(t, evals) - }) -} - -// --------------------------------------------------------------------------- -// EnvironmentVersionRuleEvaluators tests -// --------------------------------------------------------------------------- - -func TestEnvironmentVersionRuleEvaluators(t *testing.T) { +func TestRuleEvaluators(t *testing.T) { getter := &mockGetter{} t.Run("returns empty for nil rule", func(t *testing.T) { - evals := EnvironmentVersionRuleEvaluators(getter, nil) + evals := RuleEvaluators(getter, nil) assert.Empty(t, evals) }) - t.Run("returns empty for nil getter", func(t *testing.T) { - evals := EnvironmentVersionRuleEvaluators(nil, ruleWithApproval("r-1")) - assert.Empty(t, evals) - }) - - t.Run("returns empty for nil getter and nil rule", func(t *testing.T) { - evals := EnvironmentVersionRuleEvaluators(nil, nil) - assert.Empty(t, evals) + t.Run("returns empty for nil getter with non-nil rule", func(t *testing.T) { + evals := RuleEvaluators(nil, ruleWithApproval("r-1")) + // deployment window doesn't need a getter, so it may still return + // but approval/envprogression/cooldown need a getter and return nil + for _, e := range evals { + assert.NotEqual(t, evaluator.RuleTypeApproval, e.RuleType()) + assert.NotEqual(t, evaluator.RuleTypeEnvironmentProgression, e.RuleType()) + assert.NotEqual(t, evaluator.RuleTypeVersionCooldown, e.RuleType()) + } }) t.Run("returns empty for rule without relevant fields", func(t *testing.T) { - evals := EnvironmentVersionRuleEvaluators(getter, emptyRule("r-1")) - assert.Empty(t, evals) - }) - - t.Run("returns empty for rule with only deployment window", func(t *testing.T) { - evals := EnvironmentVersionRuleEvaluators(getter, ruleWithDeploymentWindow("r-1")) + evals := RuleEvaluators(getter, emptyRule("r-1")) assert.Empty(t, evals) }) - t.Run("returns empty for rule with only version cooldown", func(t *testing.T) { - evals := EnvironmentVersionRuleEvaluators(getter, ruleWithVersionCooldown("r-1")) - assert.Empty(t, evals) + t.Run("returns deployment window evaluator", func(t *testing.T) { + evals := RuleEvaluators(getter, ruleWithDeploymentWindow("r-1")) + require.Len(t, evals, 1) + assert.Equal(t, evaluator.RuleTypeDeploymentWindow, evals[0].RuleType()) }) t.Run("returns approval evaluator", func(t *testing.T) { - evals := EnvironmentVersionRuleEvaluators(getter, ruleWithApproval("r-1")) + evals := RuleEvaluators(getter, ruleWithApproval("r-1")) require.Len(t, evals, 1) assert.Equal(t, evaluator.RuleTypeApproval, evals[0].RuleType()) }) t.Run("returns environment progression evaluator", func(t *testing.T) { - evals := EnvironmentVersionRuleEvaluators(getter, ruleWithEnvironmentProgression("r-1")) + evals := RuleEvaluators(getter, ruleWithEnvironmentProgression("r-1")) require.Len(t, evals, 1) assert.Equal(t, evaluator.RuleTypeEnvironmentProgression, evals[0].RuleType()) }) - t.Run("returns both approval and environment progression when both present", func(t *testing.T) { - rule := &oapi.PolicyRule{ - Id: "r-1", - AnyApproval: &oapi.AnyApprovalRule{MinApprovals: 1}, - EnvironmentProgression: &oapi.EnvironmentProgressionRule{}, - } - evals := EnvironmentVersionRuleEvaluators(getter, rule) - require.Len(t, evals, 2) - types := evalTypes(evals) - assert.Contains(t, types, evaluator.RuleTypeApproval) - assert.Contains(t, types, evaluator.RuleTypeEnvironmentProgression) - }) - - t.Run("ignores deployment window and version cooldown in same rule", func(t *testing.T) { - rule := &oapi.PolicyRule{ - Id: "r-1", - AnyApproval: &oapi.AnyApprovalRule{MinApprovals: 1}, - DeploymentWindow: &oapi.DeploymentWindowRule{ - Rrule: "FREQ=WEEKLY;BYDAY=MO", - DurationMinutes: 30, - }, - VersionCooldown: &oapi.VersionCooldownRule{IntervalSeconds: 60}, - } - evals := EnvironmentVersionRuleEvaluators(getter, rule) - types := evalTypes(evals) - assert.NotContains(t, types, evaluator.RuleTypeDeploymentWindow) - assert.NotContains(t, types, evaluator.RuleTypeVersionCooldown) - assert.Contains(t, types, evaluator.RuleTypeApproval) - }) - - t.Run("approval evaluator scope includes environment and version", func(t *testing.T) { - evals := EnvironmentVersionRuleEvaluators(getter, ruleWithApproval("r-1")) - require.Len(t, evals, 1) - sf := evals[0].ScopeFields() - assert.NotZero(t, sf&evaluator.ScopeEnvironment) - assert.NotZero(t, sf&evaluator.ScopeVersion) - }) -} - -// --------------------------------------------------------------------------- -// DeploymentVersionRuleEvaluators tests -// --------------------------------------------------------------------------- - -func TestDeploymentVersionRuleEvaluators(t *testing.T) { - getter := &mockGetter{} - - t.Run("returns empty for nil rule", func(t *testing.T) { - evals := DeploymentVersionRuleEvaluators(getter, nil) - assert.Empty(t, evals) - }) - - t.Run("returns empty for nil getter", func(t *testing.T) { - evals := DeploymentVersionRuleEvaluators(nil, ruleWithVersionCooldown("r-1")) - assert.Empty(t, evals) - }) - - t.Run("returns empty for nil getter and nil rule", func(t *testing.T) { - evals := DeploymentVersionRuleEvaluators(nil, nil) - assert.Empty(t, evals) - }) - - t.Run("returns empty for rule without relevant fields", func(t *testing.T) { - evals := DeploymentVersionRuleEvaluators(getter, emptyRule("r-1")) - assert.Empty(t, evals) - }) - - t.Run("returns empty for rule with only deployment window", func(t *testing.T) { - evals := DeploymentVersionRuleEvaluators(getter, ruleWithDeploymentWindow("r-1")) - assert.Empty(t, evals) - }) - - t.Run("returns empty for rule with only approval", func(t *testing.T) { - evals := DeploymentVersionRuleEvaluators(getter, ruleWithApproval("r-1")) - assert.Empty(t, evals) - }) - - t.Run("returns empty for rule with only environment progression", func(t *testing.T) { - evals := DeploymentVersionRuleEvaluators(getter, ruleWithEnvironmentProgression("r-1")) - assert.Empty(t, evals) - }) - t.Run("returns version cooldown evaluator", func(t *testing.T) { - evals := DeploymentVersionRuleEvaluators(getter, ruleWithVersionCooldown("r-1")) + evals := RuleEvaluators(getter, ruleWithVersionCooldown("r-1")) require.Len(t, evals, 1) assert.Equal(t, evaluator.RuleTypeVersionCooldown, evals[0].RuleType()) }) - t.Run("version cooldown evaluator scope includes version", func(t *testing.T) { - evals := DeploymentVersionRuleEvaluators(getter, ruleWithVersionCooldown("r-1")) - require.Len(t, evals, 1) - assert.NotZero(t, evals[0].ScopeFields()&evaluator.ScopeVersion) - }) - - t.Run("ignores deployment window and approval in same rule", func(t *testing.T) { + t.Run("returns all evaluators for rule with all fields", func(t *testing.T) { rule := &oapi.PolicyRule{ - Id: "r-1", + Id: "r-all", AnyApproval: &oapi.AnyApprovalRule{MinApprovals: 1}, DeploymentWindow: &oapi.DeploymentWindowRule{ - Rrule: "FREQ=WEEKLY;BYDAY=MO", - DurationMinutes: 30, + Rrule: "FREQ=DAILY;BYHOUR=9", + DurationMinutes: 60, }, - VersionCooldown: &oapi.VersionCooldownRule{IntervalSeconds: 300}, + EnvironmentProgression: &oapi.EnvironmentProgressionRule{}, + VersionCooldown: &oapi.VersionCooldownRule{IntervalSeconds: 300}, } - evals := DeploymentVersionRuleEvaluators(getter, rule) - types := evalTypes(evals) - assert.NotContains(t, types, evaluator.RuleTypeDeploymentWindow) - assert.NotContains(t, types, evaluator.RuleTypeApproval) - assert.Contains(t, types, evaluator.RuleTypeVersionCooldown) - }) -} - -// --------------------------------------------------------------------------- -// Cross-function isolation: each scope function only returns its own evaluators -// --------------------------------------------------------------------------- - -func TestScopeIsolation(t *testing.T) { - getter := &mockGetter{} - - rule := &oapi.PolicyRule{ - Id: "r-all", - AnyApproval: &oapi.AnyApprovalRule{MinApprovals: 1}, - DeploymentWindow: &oapi.DeploymentWindowRule{ - Rrule: "FREQ=DAILY;BYHOUR=9", - DurationMinutes: 60, - }, - EnvironmentProgression: &oapi.EnvironmentProgressionRule{}, - VersionCooldown: &oapi.VersionCooldownRule{IntervalSeconds: 300}, - } - - t.Run("environment scope only returns deployment window", func(t *testing.T) { - evals := EnvironmentRuleEvaluators(rule) - types := evalTypes(evals) - assert.Equal(t, []string{evaluator.RuleTypeDeploymentWindow}, types) - }) - - t.Run("environment-version scope only returns approval and env progression", func(t *testing.T) { - evals := EnvironmentVersionRuleEvaluators(getter, rule) + evals := RuleEvaluators(getter, rule) types := evalTypes(evals) - assert.Len(t, types, 2) - assert.Contains(t, types, evaluator.RuleTypeApproval) - assert.Contains(t, types, evaluator.RuleTypeEnvironmentProgression) - assert.NotContains(t, types, evaluator.RuleTypeDeploymentWindow) - assert.NotContains(t, types, evaluator.RuleTypeVersionCooldown) - }) - - t.Run("deployment-version scope only returns version cooldown", func(t *testing.T) { - evals := DeploymentVersionRuleEvaluators(getter, rule) - types := evalTypes(evals) - assert.Equal(t, []string{evaluator.RuleTypeVersionCooldown}, types) - }) - - t.Run("union of all scopes covers all four evaluator types", func(t *testing.T) { - envEvals := EnvironmentRuleEvaluators(rule) - envVerEvals := EnvironmentVersionRuleEvaluators(getter, rule) - depVerEvals := DeploymentVersionRuleEvaluators(getter, rule) - - all := append(append(envEvals, envVerEvals...), depVerEvals...) - types := evalTypes(all) assert.Contains(t, types, evaluator.RuleTypeDeploymentWindow) assert.Contains(t, types, evaluator.RuleTypeApproval) assert.Contains(t, types, evaluator.RuleTypeEnvironmentProgression) diff --git a/packages/db/drizzle/0162_same_kronos.sql b/packages/db/drizzle/0162_bizarre_jackal.sql similarity index 54% rename from packages/db/drizzle/0162_same_kronos.sql rename to packages/db/drizzle/0162_bizarre_jackal.sql index 724d712f4..497c6e13e 100644 --- a/packages/db/drizzle/0162_same_kronos.sql +++ b/packages/db/drizzle/0162_bizarre_jackal.sql @@ -1,9 +1,8 @@ CREATE TABLE "policy_rule_summary" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "rule_id" uuid NOT NULL, - "deployment_id" uuid, - "environment_id" uuid, - "version_id" uuid, + "environment_id" uuid NOT NULL, + "version_id" uuid NOT NULL, "allowed" boolean NOT NULL, "action_required" boolean DEFAULT false NOT NULL, "action_type" text, @@ -14,9 +13,7 @@ CREATE TABLE "policy_rule_summary" ( "evaluated_at" timestamp with time zone DEFAULT now() NOT NULL ); --> statement-breakpoint -ALTER TABLE "policy_rule_summary" ADD CONSTRAINT "policy_rule_summary_deployment_id_deployment_id_fk" FOREIGN KEY ("deployment_id") REFERENCES "public"."deployment"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "policy_rule_summary" ADD CONSTRAINT "policy_rule_summary_environment_id_environment_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environment"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "policy_rule_summary" ADD CONSTRAINT "policy_rule_summary_version_id_deployment_version_id_fk" FOREIGN KEY ("version_id") REFERENCES "public"."deployment_version"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -CREATE UNIQUE INDEX "policy_rule_summary_rule_id_deployment_id_environment_id_version_id_index" ON "policy_rule_summary" USING btree ("rule_id","deployment_id","environment_id","version_id");--> statement-breakpoint -CREATE INDEX "policy_rule_summary_deployment_id_version_id_index" ON "policy_rule_summary" USING btree ("deployment_id","version_id");--> statement-breakpoint -CREATE INDEX "policy_rule_summary_environment_id_index" ON "policy_rule_summary" USING btree ("environment_id"); \ No newline at end of file +CREATE UNIQUE INDEX "policy_rule_summary_rule_id_environment_id_version_id_index" ON "policy_rule_summary" USING btree ("rule_id","environment_id","version_id");--> statement-breakpoint +CREATE INDEX "policy_rule_summary_environment_id_version_id_index" ON "policy_rule_summary" USING btree ("environment_id","version_id"); \ No newline at end of file diff --git a/packages/db/drizzle/meta/0162_snapshot.json b/packages/db/drizzle/meta/0162_snapshot.json index cfc01de5a..66444aaca 100644 --- a/packages/db/drizzle/meta/0162_snapshot.json +++ b/packages/db/drizzle/meta/0162_snapshot.json @@ -1,5 +1,5 @@ { - "id": "3df5913d-902e-428c-a708-251431c2beec", + "id": "d464f8e2-1ddf-4304-ad5e-56e0af89c19a", "prevId": "b0a123af-2a56-4139-9a3f-bc2d37bbb11b", "version": "7", "dialect": "postgresql", @@ -5006,23 +5006,17 @@ "primaryKey": false, "notNull": true }, - "deployment_id": { - "name": "deployment_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, "environment_id": { "name": "environment_id", "type": "uuid", "primaryKey": false, - "notNull": false + "notNull": true }, "version_id": { "name": "version_id", "type": "uuid", "primaryKey": false, - "notNull": false + "notNull": true }, "allowed": { "name": "allowed", @@ -5077,8 +5071,8 @@ } }, "indexes": { - "policy_rule_summary_rule_id_deployment_id_environment_id_version_id_index": { - "name": "policy_rule_summary_rule_id_deployment_id_environment_id_version_id_index", + "policy_rule_summary_rule_id_environment_id_version_id_index": { + "name": "policy_rule_summary_rule_id_environment_id_version_id_index", "columns": [ { "expression": "rule_id", @@ -5086,12 +5080,6 @@ "asc": true, "nulls": "last" }, - { - "expression": "deployment_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, { "expression": "environment_id", "isExpression": false, @@ -5110,11 +5098,11 @@ "method": "btree", "with": {} }, - "policy_rule_summary_deployment_id_version_id_index": { - "name": "policy_rule_summary_deployment_id_version_id_index", + "policy_rule_summary_environment_id_version_id_index": { + "name": "policy_rule_summary_environment_id_version_id_index", "columns": [ { - "expression": "deployment_id", + "expression": "environment_id", "isExpression": false, "asc": true, "nulls": "last" @@ -5130,37 +5118,9 @@ "concurrently": false, "method": "btree", "with": {} - }, - "policy_rule_summary_environment_id_index": { - "name": "policy_rule_summary_environment_id_index", - "columns": [ - { - "expression": "environment_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} } }, "foreignKeys": { - "policy_rule_summary_deployment_id_deployment_id_fk": { - "name": "policy_rule_summary_deployment_id_deployment_id_fk", - "tableFrom": "policy_rule_summary", - "tableTo": "deployment", - "columnsFrom": [ - "deployment_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, "policy_rule_summary_environment_id_environment_id_fk": { "name": "policy_rule_summary_environment_id_environment_id_fk", "tableFrom": "policy_rule_summary", diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 32328643b..21ee7376f 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -1139,8 +1139,8 @@ { "idx": 162, "version": "7", - "when": 1772850815650, - "tag": "0162_same_kronos", + "when": 1772855230121, + "tag": "0162_bizarre_jackal", "breakpoints": true } ] diff --git a/packages/db/src/reconcilers/index.ts b/packages/db/src/reconcilers/index.ts index 81693bf79..d195ef5c6 100644 --- a/packages/db/src/reconcilers/index.ts +++ b/packages/db/src/reconcilers/index.ts @@ -220,6 +220,43 @@ export async function enqueueManyRelationshipEval( ); } +// --------------------------------------------------------------------------- +// Policy summary +// --------------------------------------------------------------------------- + +const POLICY_SUMMARY_KIND = "policy-summary"; + +export async function enqueuePolicySummary( + db: Tx, + params: { workspaceId: string; environmentId: string; versionId: string }, +): Promise { + return enqueue(db, { + workspaceId: params.workspaceId, + kind: POLICY_SUMMARY_KIND, + scopeType: "environment-version", + scopeId: `${params.environmentId}:${params.versionId}`, + }); +} + +export async function enqueueManyPolicySummary( + db: Tx, + items: Array<{ + workspaceId: string; + environmentId: string; + versionId: string; + }>, +): Promise { + return enqueueMany( + db, + items.map((item) => ({ + workspaceId: item.workspaceId, + kind: POLICY_SUMMARY_KIND, + scopeType: "environment-version", + scopeId: `${item.environmentId}:${item.versionId}`, + })), + ); +} + // --------------------------------------------------------------------------- // Desired release // --------------------------------------------------------------------------- diff --git a/packages/db/src/schema/policy-rule-summary.ts b/packages/db/src/schema/policy-rule-summary.ts index a49d12eee..295308daa 100644 --- a/packages/db/src/schema/policy-rule-summary.ts +++ b/packages/db/src/schema/policy-rule-summary.ts @@ -10,7 +10,6 @@ import { uuid, } from "drizzle-orm/pg-core"; -import { deployment } from "./deployment.js"; import { deploymentVersion } from "./deployment-version.js"; import { environment } from "./environment.js"; @@ -20,15 +19,12 @@ export const policyRuleSummary = pgTable( id: uuid("id").primaryKey().defaultRandom(), ruleId: uuid("rule_id").notNull(), - deploymentId: uuid("deployment_id").references(() => deployment.id, { - onDelete: "cascade", - }), - environmentId: uuid("environment_id").references(() => environment.id, { - onDelete: "cascade", - }), - versionId: uuid("version_id").references(() => deploymentVersion.id, { - onDelete: "cascade", - }), + environmentId: uuid("environment_id") + .notNull() + .references(() => environment.id, { onDelete: "cascade" }), + versionId: uuid("version_id") + .notNull() + .references(() => deploymentVersion.id, { onDelete: "cascade" }), allowed: boolean("allowed").notNull(), actionRequired: boolean("action_required").notNull().default(false), @@ -43,19 +39,14 @@ export const policyRuleSummary = pgTable( .defaultNow(), }, (t) => [ - uniqueIndex().on(t.ruleId, t.deploymentId, t.environmentId, t.versionId), - index().on(t.deploymentId, t.versionId), - index().on(t.environmentId), + uniqueIndex().on(t.ruleId, t.environmentId, t.versionId), + index().on(t.environmentId, t.versionId), ], ); export const policyRuleSummaryRelations = relations( policyRuleSummary, ({ one }) => ({ - deployment: one(deployment, { - fields: [policyRuleSummary.deploymentId], - references: [deployment.id], - }), environment: one(environment, { fields: [policyRuleSummary.environmentId], references: [environment.id], diff --git a/packages/trpc/src/routes/reconcile.ts b/packages/trpc/src/routes/reconcile.ts index 58c840661..39ee0c5a3 100644 --- a/packages/trpc/src/routes/reconcile.ts +++ b/packages/trpc/src/routes/reconcile.ts @@ -15,6 +15,7 @@ import { enqueueDeploymentSelectorEval, enqueueEnvironmentSelectorEval, enqueueManyRelationshipEval, + enqueuePolicySummary, enqueueRelationshipEval, } from "@ctrlplane/db/reconcilers"; import * as schema from "@ctrlplane/db/schema"; @@ -133,6 +134,16 @@ export const reconcileRouter = router({ return { enqueued: items.length }; }), + triggerPolicySummary: protectedProcedure + .input( + z.object({ + workspaceId: z.string().uuid(), + environmentId: z.string().uuid(), + versionId: z.string().uuid(), + }), + ) + .mutation(({ ctx, input }) => enqueuePolicySummary(ctx.db, input)), + listWorkScopes: protectedProcedure .input( z.object({ From eac5709156e6bc3855d914990e5602a20a9509f6 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Fri, 6 Mar 2026 20:13:18 -0800 Subject: [PATCH 06/11] update --- .../controllers/policysummary/reconcile.go | 2 +- .../summaryeval/getter_postgres.go | 10 +++----- .../policysummary/summaryeval/summaryeval.go | 4 ++-- .../summaryeval/summaryeval_test.go | 24 ++++++++++--------- 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/apps/workspace-engine/svc/controllers/policysummary/reconcile.go b/apps/workspace-engine/svc/controllers/policysummary/reconcile.go index df0004a60..ff093872a 100644 --- a/apps/workspace-engine/svc/controllers/policysummary/reconcile.go +++ b/apps/workspace-engine/svc/controllers/policysummary/reconcile.go @@ -50,7 +50,7 @@ func (r *reconciler) reconcile(ctx context.Context, scope *Scope) (*ReconcileRes for _, p := range policies { for _, rule := range p.Rules { - evals := summaryeval.RuleEvaluators(r.getter, &rule) + evals := summaryeval.RuleEvaluators(r.getter, r.workspaceID.String(), &rule) for _, eval := range evals { if eval == nil { continue diff --git a/apps/workspace-engine/svc/controllers/policysummary/summaryeval/getter_postgres.go b/apps/workspace-engine/svc/controllers/policysummary/summaryeval/getter_postgres.go index 6e29ab043..80e4247d8 100644 --- a/apps/workspace-engine/svc/controllers/policysummary/summaryeval/getter_postgres.go +++ b/apps/workspace-engine/svc/controllers/policysummary/summaryeval/getter_postgres.go @@ -1,9 +1,9 @@ package summaryeval import ( + "context" "workspace-engine/pkg/db" "workspace-engine/pkg/oapi" - "workspace-engine/pkg/workspace/releasemanager/policy/evaluator" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versioncooldown" ) @@ -26,12 +26,8 @@ func (g *PostgresGetter) GetJobVerificationStatus(jobID string) oapi.JobVerifica return g.versioncooldown.GetJobVerificationStatus(jobID) } -func (g *PostgresGetter) NewVersionCooldownEvaluator(rule *oapi.PolicyRule) evaluator.Evaluator { - return g.versioncooldown.NewVersionCooldownEvaluator(rule) -} - -func (g *PostgresGetter) GetReleaseTargets() ([]*oapi.ReleaseTarget, error) { - return g.versioncooldown.GetReleaseTargets() +func (g *PostgresGetter) GetAllReleaseTargets(ctx context.Context, workspaceID string) ([]*oapi.ReleaseTarget, error) { + return g.versioncooldown.GetAllReleaseTargets(ctx, workspaceID) } func (g *PostgresGetter) GetJobsForReleaseTarget(releaseTarget *oapi.ReleaseTarget) map[string]*oapi.Job { diff --git a/apps/workspace-engine/svc/controllers/policysummary/summaryeval/summaryeval.go b/apps/workspace-engine/svc/controllers/policysummary/summaryeval/summaryeval.go index 85f6b1f69..3905ea1a8 100644 --- a/apps/workspace-engine/svc/controllers/policysummary/summaryeval/summaryeval.go +++ b/apps/workspace-engine/svc/controllers/policysummary/summaryeval/summaryeval.go @@ -10,12 +10,12 @@ import ( ) // RuleEvaluators returns all summary evaluators for a given policy rule. -func RuleEvaluators(getter Getter, rule *oapi.PolicyRule) []evaluator.Evaluator { +func RuleEvaluators(getter Getter, wsId string, rule *oapi.PolicyRule) []evaluator.Evaluator { return evaluator.CollectEvaluators( deploymentwindow.NewSummaryEvaluator(rule), approval.NewEvaluator(getter, rule), environmentprogression.NewEvaluator(getter, rule), - versioncooldown.NewSummaryEvaluator(getter, rule), + versioncooldown.NewSummaryEvaluator(getter, wsId, rule), // TODO: add gradualrollout.NewSummaryEvaluator(getter, rule) // once the getter-based constructor is added ) diff --git a/apps/workspace-engine/svc/controllers/policysummary/summaryeval/summaryeval_test.go b/apps/workspace-engine/svc/controllers/policysummary/summaryeval/summaryeval_test.go index 6386c6fe2..8ac6c4fd4 100644 --- a/apps/workspace-engine/svc/controllers/policysummary/summaryeval/summaryeval_test.go +++ b/apps/workspace-engine/svc/controllers/policysummary/summaryeval/summaryeval_test.go @@ -7,6 +7,7 @@ import ( "workspace-engine/pkg/oapi" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -63,12 +64,12 @@ func (m *mockGetter) HasCurrentRelease(_ context.Context, _ *oapi.ReleaseTarget) return false, nil } func (m *mockGetter) GetReleaseTargets() ([]*oapi.ReleaseTarget, error) { return nil, nil } +func (m *mockGetter) GetAllReleaseTargets(_ context.Context, _ string) ([]*oapi.ReleaseTarget, error) { + return nil, nil +} func (m *mockGetter) GetJobVerificationStatus(_ string) oapi.JobVerificationStatus { return oapi.JobVerificationStatusCancelled } -func (m *mockGetter) NewVersionCooldownEvaluator(rule *oapi.PolicyRule) evaluator.Evaluator { - return nil -} // --------------------------------------------------------------------------- // Rule builder helpers @@ -123,14 +124,15 @@ func evalTypes(evals []evaluator.Evaluator) []string { func TestRuleEvaluators(t *testing.T) { getter := &mockGetter{} + wsId := uuid.New().String() t.Run("returns empty for nil rule", func(t *testing.T) { - evals := RuleEvaluators(getter, nil) + evals := RuleEvaluators(getter, wsId, nil) assert.Empty(t, evals) }) t.Run("returns empty for nil getter with non-nil rule", func(t *testing.T) { - evals := RuleEvaluators(nil, ruleWithApproval("r-1")) + evals := RuleEvaluators(nil, wsId, ruleWithApproval("r-1")) // deployment window doesn't need a getter, so it may still return // but approval/envprogression/cooldown need a getter and return nil for _, e := range evals { @@ -141,30 +143,30 @@ func TestRuleEvaluators(t *testing.T) { }) t.Run("returns empty for rule without relevant fields", func(t *testing.T) { - evals := RuleEvaluators(getter, emptyRule("r-1")) + evals := RuleEvaluators(getter, wsId, emptyRule("r-1")) assert.Empty(t, evals) }) t.Run("returns deployment window evaluator", func(t *testing.T) { - evals := RuleEvaluators(getter, ruleWithDeploymentWindow("r-1")) + evals := RuleEvaluators(getter, wsId, ruleWithDeploymentWindow("r-1")) require.Len(t, evals, 1) assert.Equal(t, evaluator.RuleTypeDeploymentWindow, evals[0].RuleType()) }) t.Run("returns approval evaluator", func(t *testing.T) { - evals := RuleEvaluators(getter, ruleWithApproval("r-1")) + evals := RuleEvaluators(getter, wsId, ruleWithApproval("r-1")) require.Len(t, evals, 1) assert.Equal(t, evaluator.RuleTypeApproval, evals[0].RuleType()) }) t.Run("returns environment progression evaluator", func(t *testing.T) { - evals := RuleEvaluators(getter, ruleWithEnvironmentProgression("r-1")) + evals := RuleEvaluators(getter, wsId, ruleWithEnvironmentProgression("r-1")) require.Len(t, evals, 1) assert.Equal(t, evaluator.RuleTypeEnvironmentProgression, evals[0].RuleType()) }) t.Run("returns version cooldown evaluator", func(t *testing.T) { - evals := RuleEvaluators(getter, ruleWithVersionCooldown("r-1")) + evals := RuleEvaluators(getter, wsId, ruleWithVersionCooldown("r-1")) require.Len(t, evals, 1) assert.Equal(t, evaluator.RuleTypeVersionCooldown, evals[0].RuleType()) }) @@ -180,7 +182,7 @@ func TestRuleEvaluators(t *testing.T) { EnvironmentProgression: &oapi.EnvironmentProgressionRule{}, VersionCooldown: &oapi.VersionCooldownRule{IntervalSeconds: 300}, } - evals := RuleEvaluators(getter, rule) + evals := RuleEvaluators(getter, wsId, rule) types := evalTypes(evals) assert.Contains(t, types, evaluator.RuleTypeDeploymentWindow) assert.Contains(t, types, evaluator.RuleTypeApproval) From 082a479156c112fab1667c17ca59323346de183f Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Fri, 6 Mar 2026 20:20:12 -0800 Subject: [PATCH 07/11] fmt --- packages/db/drizzle/meta/0162_snapshot.json | 655 +++++--------------- packages/db/drizzle/meta/_journal.json | 2 +- 2 files changed, 161 insertions(+), 496 deletions(-) diff --git a/packages/db/drizzle/meta/0162_snapshot.json b/packages/db/drizzle/meta/0162_snapshot.json index 66444aaca..acaa4c91f 100644 --- a/packages/db/drizzle/meta/0162_snapshot.json +++ b/packages/db/drizzle/meta/0162_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" } @@ -1019,12 +985,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" } @@ -1093,12 +1055,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" } @@ -1108,10 +1066,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": {}, @@ -1177,12 +1132,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" } @@ -1314,12 +1265,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" } @@ -1366,12 +1313,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" }, @@ -1379,12 +1322,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" } @@ -1392,10 +1331,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": {}, @@ -1489,12 +1425,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" } @@ -1541,12 +1473,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" }, @@ -1554,12 +1482,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" } @@ -1567,10 +1491,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": {}, @@ -1652,12 +1573,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" } @@ -1711,12 +1628,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" } @@ -1872,12 +1785,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" }, @@ -1885,12 +1794,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" } @@ -1971,12 +1876,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" } @@ -2053,12 +1954,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" } @@ -2113,12 +2010,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" } @@ -2159,12 +2052,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" }, @@ -2172,12 +2061,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" } @@ -2185,10 +2070,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": {}, @@ -2226,12 +2108,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" }, @@ -2239,12 +2117,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" } @@ -2252,10 +2126,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": {}, @@ -2293,12 +2164,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" } @@ -2361,12 +2228,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" }, @@ -2374,12 +2237,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" } @@ -2534,12 +2393,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" } @@ -2623,12 +2478,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" } @@ -2704,12 +2555,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" } @@ -2758,9 +2605,7 @@ "workspace_slug_unique": { "name": "workspace_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] } }, "policies": {}, @@ -2851,12 +2696,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" }, @@ -2864,12 +2705,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" } @@ -2929,12 +2766,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" }, @@ -2942,12 +2775,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" }, @@ -2955,12 +2784,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" } @@ -2970,9 +2795,7 @@ "workspace_invite_token_token_unique": { "name": "workspace_invite_token_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -3069,12 +2892,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" } @@ -3121,12 +2940,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" } @@ -3189,12 +3004,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" } @@ -3282,12 +3093,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" }, @@ -3295,12 +3102,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" }, @@ -3308,12 +3111,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" }, @@ -3321,12 +3120,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" } @@ -3410,12 +3205,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" }, @@ -3423,12 +3214,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" } @@ -3511,12 +3298,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" } @@ -3652,12 +3435,8 @@ "name": "reconcile_work_payload_scope_ref_reconcile_work_scope_id_fk", "tableFrom": "reconcile_work_payload", "tableTo": "reconcile_work_scope", - "columnsFrom": [ - "scope_ref" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["scope_ref"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3931,12 +3710,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" } @@ -3984,12 +3759,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" } @@ -4037,12 +3808,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" } @@ -4108,12 +3875,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" } @@ -4185,12 +3948,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" } @@ -4244,12 +4003,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" } @@ -4321,12 +4076,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" } @@ -4380,12 +4131,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" } @@ -4440,12 +4187,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" } @@ -4493,12 +4236,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" } @@ -4552,12 +4291,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" } @@ -4615,11 +4350,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": {}, @@ -4656,12 +4387,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" } @@ -4669,10 +4396,7 @@ "compositePrimaryKeys": { "resource_variable_resource_id_key_pk": { "name": "resource_variable_resource_id_key_pk", - "columns": [ - "resource_id", - "key" - ] + "columns": ["resource_id", "key"] } }, "uniqueConstraints": {}, @@ -4724,12 +4448,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" } @@ -4784,12 +4504,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" } @@ -4855,12 +4571,8 @@ "name": "workflow_job_template_workflow_id_workflow_id_fk", "tableFrom": "workflow_job_template", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4902,12 +4614,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" } @@ -5125,12 +4833,8 @@ "name": "policy_rule_summary_environment_id_environment_id_fk", "tableFrom": "policy_rule_summary", "tableTo": "environment", - "columnsFrom": [ - "environment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["environment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5138,12 +4842,8 @@ "name": "policy_rule_summary_version_id_deployment_version_id_fk", "tableFrom": "policy_rule_summary", "tableTo": "deployment_version", - "columnsFrom": [ - "version_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["version_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5222,12 +4922,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" } @@ -5421,12 +5117,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" } @@ -5485,12 +5177,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" } @@ -5604,12 +5292,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" } @@ -5685,12 +5369,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" } @@ -5706,10 +5386,7 @@ "public.system_role": { "name": "system_role", "schema": "public", - "values": [ - "user", - "admin" - ] + "values": ["user", "admin"] }, "public.deployment_version_status": { "name": "deployment_version_status", @@ -5752,10 +5429,7 @@ "public.entity_type": { "name": "entity_type", "schema": "public", - "values": [ - "user", - "team" - ] + "values": ["user", "team"] }, "public.scope_type": { "name": "scope_type", @@ -5773,21 +5447,12 @@ "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"] } }, "schemas": {}, @@ -5800,4 +5465,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 21ee7376f..3c891c5e3 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -1144,4 +1144,4 @@ "breakpoints": true } ] -} \ No newline at end of file +} From 5266303afd2eb447ea7dcb3c4b66b05ef45b4941 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Fri, 6 Mar 2026 21:49:39 -0800 Subject: [PATCH 08/11] fix --- .../work-queue/CreateWorkItemDialog.tsx | 67 +++++++++++++++++++ .../controllers/policysummary/reconcile.go | 7 ++ 2 files changed, 74 insertions(+) diff --git a/apps/web/app/routes/ws/settings/_components/work-queue/CreateWorkItemDialog.tsx b/apps/web/app/routes/ws/settings/_components/work-queue/CreateWorkItemDialog.tsx index bf3921497..3a4537d1c 100644 --- a/apps/web/app/routes/ws/settings/_components/work-queue/CreateWorkItemDialog.tsx +++ b/apps/web/app/routes/ws/settings/_components/work-queue/CreateWorkItemDialog.tsx @@ -31,6 +31,7 @@ type Template = | "environment-selector" | "relationship-entity" | "relationship-rule" + | "policy-summary" | "advanced"; const TEMPLATES: { value: Template; label: string; description: string }[] = [ @@ -57,6 +58,12 @@ const TEMPLATES: { value: Template; label: string; description: string }[] = [ description: "Re-evaluate relationships for all entities in the workspace. Useful after changing a rule.", }, + { + value: "policy-summary", + label: "Policy Summary Eval", + description: + "Re-evaluate policy summaries for an environment + version pair.", + }, { value: "advanced", label: "Advanced", @@ -307,6 +314,63 @@ function RelationshipRuleForm({ ); } +function PolicySummaryForm({ + workspaceId, + onDone, +}: { + workspaceId: string; + onDone: () => void; +}) { + const [environmentId, setEnvironmentId] = useState(""); + const [versionId, setVersionId] = useState(""); + const invalidate = useInvalidateAll(); + const mutation = trpc.reconcile.triggerPolicySummary.useMutation({ + onSuccess: () => { + invalidate(); + onDone(); + }, + }); + + return ( +
{ + e.preventDefault(); + mutation.mutate({ workspaceId, environmentId, versionId }); + }} + className="flex flex-col gap-4" + > +
+ + setEnvironmentId(e.target.value)} + required + /> +
+
+ + setVersionId(e.target.value)} + required + /> +
+ + + + {mutation.error && ( +

{mutation.error.message}

+ )} +
+ ); +} + function AdvancedForm({ workspaceId, onDone, @@ -579,6 +643,9 @@ export const CreateWorkItemDialog: React.FC<{ {template === "relationship-rule" && ( )} + {template === "policy-summary" && ( + + )} {template === "advanced" && ( )} diff --git a/apps/workspace-engine/svc/controllers/policysummary/reconcile.go b/apps/workspace-engine/svc/controllers/policysummary/reconcile.go index ff093872a..e4c65fbc7 100644 --- a/apps/workspace-engine/svc/controllers/policysummary/reconcile.go +++ b/apps/workspace-engine/svc/controllers/policysummary/reconcile.go @@ -9,6 +9,8 @@ import ( "workspace-engine/svc/controllers/policysummary/summaryeval" "github.com/google/uuid" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" ) type ReconcileResult struct { @@ -41,6 +43,7 @@ func (r *reconciler) reconcile(ctx context.Context, scope *Scope) (*ReconcileRes } policies, err := r.getter.GetPoliciesForEnvironment(ctx, r.workspaceID, scope.EnvironmentID) + span.SetAttributes(attribute.Int("policies_count", len(policies))) if err != nil { return nil, fmt.Errorf("get policies: %w", err) } @@ -49,8 +52,10 @@ func (r *reconciler) reconcile(ctx context.Context, scope *Scope) (*ReconcileRes var nextTime *time.Time for _, p := range policies { + span.AddEvent("policy_rule_count", trace.WithAttributes(attribute.Int("rules_count", len(p.Rules)))) for _, rule := range p.Rules { evals := summaryeval.RuleEvaluators(r.getter, r.workspaceID.String(), &rule) + span.AddEvent("found_evaluators", trace.WithAttributes(attribute.Int("evaluators_count", len(evals)))) for _, eval := range evals { if eval == nil { continue @@ -76,6 +81,8 @@ func (r *reconciler) reconcile(ctx context.Context, scope *Scope) (*ReconcileRes } } + span.SetAttributes(attribute.Int("rows_count", len(rows))) + if err := r.setter.UpsertRuleSummaries(ctx, rows); err != nil { return nil, fmt.Errorf("upsert rule summaries: %w", err) } From f5debd601fa11c00f63f0e073b5b1de297da3733 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Fri, 6 Mar 2026 22:30:13 -0800 Subject: [PATCH 09/11] working --- .../controllers/policysummary/controller.go | 16 ++-- .../policysummary/getters_store.go | 82 +++++++++++++++++++ .../policysummary/summaryeval/getter_store.go | 35 ++++++++ 3 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 apps/workspace-engine/svc/controllers/policysummary/getters_store.go create mode 100644 apps/workspace-engine/svc/controllers/policysummary/summaryeval/getter_store.go diff --git a/apps/workspace-engine/svc/controllers/policysummary/controller.go b/apps/workspace-engine/svc/controllers/policysummary/controller.go index 42a3454d4..d1fd7e263 100644 --- a/apps/workspace-engine/svc/controllers/policysummary/controller.go +++ b/apps/workspace-engine/svc/controllers/policysummary/controller.go @@ -13,6 +13,7 @@ import ( "workspace-engine/pkg/reconcile" "workspace-engine/pkg/reconcile/events" "workspace-engine/pkg/reconcile/postgres" + "workspace-engine/pkg/workspace/manager" "github.com/jackc/pgx/v5/pgxpool" "go.opentelemetry.io/otel" @@ -24,7 +25,6 @@ var tracer = otel.Tracer("workspace-engine/svc/controllers/policysummary") var _ reconcile.Processor = (*Controller)(nil) type Controller struct { - getter Getter setter Setter } @@ -37,7 +37,14 @@ func (c *Controller) Process(ctx context.Context, item reconcile.Item) (reconcil attribute.String("item.scope_id", item.ScopeID), ) - result, err := Reconcile(ctx, item.WorkspaceID, item.ScopeID, c.getter, c.setter) + ws, ok := manager.Workspaces().Get(item.WorkspaceID) + if !ok { + return reconcile.Result{}, fmt.Errorf("workspace %s not found", item.WorkspaceID) + } + + getter := NewStoreGetter(ws) + + result, err := Reconcile(ctx, item.WorkspaceID, item.ScopeID, getter, c.setter) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) @@ -52,10 +59,6 @@ func (c *Controller) Process(ctx context.Context, item reconcile.Item) (reconcil return reconcile.Result{}, nil } -func NewController(getter Getter, setter Setter) *Controller { - return &Controller{getter: getter, setter: setter} -} - func New(workerID string, pgxPool *pgxpool.Pool) svc.Service { if pgxPool == nil { log.Fatal("Failed to get pgx pool") @@ -80,7 +83,6 @@ func New(workerID string, pgxPool *pgxpool.Pool) svc.Service { queue := postgres.NewForKinds(pgxPool, kind) queries := db.New(pgxPool) controller := &Controller{ - getter: NewPostgresGetter(queries), setter: NewPostgresSetter(queries), } worker, err := reconcile.NewWorker( diff --git a/apps/workspace-engine/svc/controllers/policysummary/getters_store.go b/apps/workspace-engine/svc/controllers/policysummary/getters_store.go new file mode 100644 index 000000000..bffa561a2 --- /dev/null +++ b/apps/workspace-engine/svc/controllers/policysummary/getters_store.go @@ -0,0 +1,82 @@ +package policysummary + +import ( + "context" + "fmt" + + "workspace-engine/pkg/oapi" + "workspace-engine/pkg/workspace" + "workspace-engine/svc/controllers/policysummary/summaryeval" + + "github.com/google/uuid" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +var storeGetterTracer = otel.Tracer("policysummary.getters_store") + +type storeEvalGetter = summaryeval.StoreGetter + +var _ Getter = (*StoreGetter)(nil) + +type StoreGetter struct { + *storeEvalGetter + ws *workspace.Workspace +} + +func NewStoreGetter(ws *workspace.Workspace) *StoreGetter { + return &StoreGetter{ + storeEvalGetter: summaryeval.NewStoreGetter(ws.Store()), + ws: ws, + } +} + +func (g *StoreGetter) GetVersion(ctx context.Context, versionID uuid.UUID) (*oapi.DeploymentVersion, error) { + ver, ok := g.ws.DeploymentVersions().Get(versionID.String()) + if !ok { + return nil, fmt.Errorf("version %s not found", versionID) + } + return ver, nil +} + +func (g *StoreGetter) GetPoliciesForEnvironment(ctx context.Context, workspaceID, environmentID uuid.UUID) ([]*oapi.Policy, error) { + ctx, span := storeGetterTracer.Start(ctx, "GetPoliciesForEnvironment") + defer span.End() + + releaseTargets, err := g.ws.ReleaseTargets().GetForEnvironment(ctx, environmentID.String()) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, "failed to get release targets for environment") + return nil, fmt.Errorf("get release targets for environment: %w", err) + } + + span.SetAttributes(attribute.Int("release_targets_count", len(releaseTargets))) + + allPolicies := make(map[string]*oapi.Policy) + + for _, rt := range releaseTargets { + policies, err := g.storeEvalGetter.GetPoliciesForReleaseTarget(ctx, rt) + span.AddEvent("get_policies_for_release_target", trace.WithAttributes(attribute.Int("policies_count", len(policies)))) + if err != nil { + return nil, fmt.Errorf("get policies for release target: %w", err) + } + for _, p := range policies { + allPolicies[p.Id] = p + } + } + + policiesSlice := make([]*oapi.Policy, 0, len(allPolicies)) + for _, p := range allPolicies { + policiesSlice = append(policiesSlice, p) + } + + span.SetAttributes(attribute.Int("policies_count", len(policiesSlice))) + + return policiesSlice, nil +} + +func (g *StoreGetter) GetPoliciesForDeployment(ctx context.Context, workspaceID, deploymentID uuid.UUID) ([]*oapi.Policy, error) { + return g.GetPoliciesForEnvironment(ctx, workspaceID, deploymentID) +} diff --git a/apps/workspace-engine/svc/controllers/policysummary/summaryeval/getter_store.go b/apps/workspace-engine/svc/controllers/policysummary/summaryeval/getter_store.go new file mode 100644 index 000000000..cd0451eb1 --- /dev/null +++ b/apps/workspace-engine/svc/controllers/policysummary/summaryeval/getter_store.go @@ -0,0 +1,35 @@ +package summaryeval + +import ( + "context" + "workspace-engine/pkg/oapi" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versioncooldown" + legacystore "workspace-engine/pkg/workspace/store" +) + +var _ Getter = (*StoreGetter)(nil) + +type StoreGetter struct { + gradualRolloutGetter + versioncooldown versioncooldown.Getters +} + +func NewStoreGetter(store *legacystore.Store) *StoreGetter { + return &StoreGetter{ + gradualRolloutGetter: gradualrollout.NewStoreGetters(store), + versioncooldown: versioncooldown.NewStoreGetters(store), + } +} + +func (g *StoreGetter) GetJobVerificationStatus(jobID string) oapi.JobVerificationStatus { + return g.versioncooldown.GetJobVerificationStatus(jobID) +} + +func (g *StoreGetter) GetAllReleaseTargets(ctx context.Context, workspaceID string) ([]*oapi.ReleaseTarget, error) { + return g.versioncooldown.GetAllReleaseTargets(ctx, workspaceID) +} + +func (g *StoreGetter) GetJobsForReleaseTarget(releaseTarget *oapi.ReleaseTarget) map[string]*oapi.Job { + return g.versioncooldown.GetJobsForReleaseTarget(releaseTarget) +} From 6760543808a230c049fac4510ff1c2188dbfcb0e Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Sat, 7 Mar 2026 08:22:48 -0800 Subject: [PATCH 10/11] fix --- .../pkg/db/computed_resources.sql.go | 44 +++++ apps/workspace-engine/pkg/db/convert.go | 161 ++++++++++++++++++ apps/workspace-engine/pkg/db/policies.sql.go | 71 ++++++++ .../pkg/db/queries/computed_resources.sql | 17 ++ .../pkg/db/queries/policies.sql | 14 ++ .../controllers/policysummary/controller.go | 16 +- .../policysummary/getters_postgres.go | 84 ++++++--- 7 files changed, 378 insertions(+), 29 deletions(-) diff --git a/apps/workspace-engine/pkg/db/computed_resources.sql.go b/apps/workspace-engine/pkg/db/computed_resources.sql.go index a7d169eaa..755a8e38d 100644 --- a/apps/workspace-engine/pkg/db/computed_resources.sql.go +++ b/apps/workspace-engine/pkg/db/computed_resources.sql.go @@ -55,6 +55,50 @@ func (q *Queries) GetReleaseTargetsForDeployment(ctx context.Context, deployment return items, nil } +const getReleaseTargetsForEnvironment = `-- name: GetReleaseTargetsForEnvironment :many +SELECT DISTINCT + cdr.deployment_id, + cer.environment_id, + cdr.resource_id +FROM computed_deployment_resource cdr +JOIN computed_environment_resource cer + ON cer.resource_id = cdr.resource_id +JOIN system_deployment sd + ON sd.deployment_id = cdr.deployment_id +JOIN system_environment se + ON se.environment_id = cer.environment_id + AND se.system_id = sd.system_id +WHERE cer.environment_id = $1 +` + +type GetReleaseTargetsForEnvironmentRow struct { + DeploymentID uuid.UUID + EnvironmentID uuid.UUID + ResourceID uuid.UUID +} + +// Returns all valid release targets for an environment by joining computed +// resource tables through the system link tables. +func (q *Queries) GetReleaseTargetsForEnvironment(ctx context.Context, environmentID uuid.UUID) ([]GetReleaseTargetsForEnvironmentRow, error) { + rows, err := q.db.Query(ctx, getReleaseTargetsForEnvironment, environmentID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetReleaseTargetsForEnvironmentRow + for rows.Next() { + var i GetReleaseTargetsForEnvironmentRow + if err := rows.Scan(&i.DeploymentID, &i.EnvironmentID, &i.ResourceID); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getReleaseTargetsForResource = `-- name: GetReleaseTargetsForResource :many SELECT DISTINCT cdr.deployment_id, diff --git a/apps/workspace-engine/pkg/db/convert.go b/apps/workspace-engine/pkg/db/convert.go index 892709408..7916e6d71 100644 --- a/apps/workspace-engine/pkg/db/convert.go +++ b/apps/workspace-engine/pkg/db/convert.go @@ -69,6 +69,167 @@ func ToOapiResource(row GetResourceByIDRow) *oapi.Resource { return r } +func ToOapiPolicyWithRules(row ListPoliciesWithRulesByWorkspaceIDRow) *oapi.Policy { + p := ToOapiPolicy(Policy{ + ID: row.ID, + Name: row.Name, + Description: row.Description, + Selector: row.Selector, + Metadata: row.Metadata, + Priority: row.Priority, + Enabled: row.Enabled, + WorkspaceID: row.WorkspaceID, + CreatedAt: row.CreatedAt, + }) + + type ruleID struct { + Id string `json:"id"` + } + + type approvalJSON struct { + Id string `json:"id"` + MinApprovals int32 `json:"minApprovals"` + } + var approvals []approvalJSON + _ = json.Unmarshal(row.ApprovalRules, &approvals) + for _, a := range approvals { + p.Rules = append(p.Rules, oapi.PolicyRule{ + Id: a.Id, + PolicyId: p.Id, + AnyApproval: &oapi.AnyApprovalRule{ + MinApprovals: a.MinApprovals, + }, + }) + } + + type windowJSON struct { + Id string `json:"id"` + AllowWindow *bool `json:"allowWindow"` + DurationMinutes int32 `json:"durationMinutes"` + Rrule string `json:"rrule"` + Timezone *string `json:"timezone"` + } + var windows []windowJSON + _ = json.Unmarshal(row.DeploymentWindowRules, &windows) + for _, w := range windows { + p.Rules = append(p.Rules, oapi.PolicyRule{ + Id: w.Id, + PolicyId: p.Id, + DeploymentWindow: &oapi.DeploymentWindowRule{ + AllowWindow: w.AllowWindow, + DurationMinutes: w.DurationMinutes, + Rrule: w.Rrule, + Timezone: w.Timezone, + }, + }) + } + + type dependencyJSON struct { + Id string `json:"id"` + DependsOn string `json:"dependsOn"` + } + var deps []dependencyJSON + _ = json.Unmarshal(row.DeploymentDependencyRules, &deps) + for _, d := range deps { + p.Rules = append(p.Rules, oapi.PolicyRule{ + Id: d.Id, + PolicyId: p.Id, + DeploymentDependency: &oapi.DeploymentDependencyRule{ + DependsOn: d.DependsOn, + }, + }) + } + + type progressionJSON struct { + Id string `json:"id"` + DependsOnEnvironmentSelector string `json:"dependsOnEnvironmentSelector"` + MaximumAgeHours *int32 `json:"maximumAgeHours"` + MinimumSoakTimeMinutes *int32 `json:"minimumSoakTimeMinutes"` + MinimumSuccessPercentage *float32 `json:"minimumSuccessPercentage"` + SuccessStatuses *[]string `json:"successStatuses"` + } + var progs []progressionJSON + _ = json.Unmarshal(row.EnvironmentProgressionRules, &progs) + for _, pr := range progs { + var depSelector oapi.Selector + _ = json.Unmarshal([]byte(pr.DependsOnEnvironmentSelector), &depSelector) + rule := oapi.EnvironmentProgressionRule{ + DependsOnEnvironmentSelector: depSelector, + MaximumAgeHours: pr.MaximumAgeHours, + MinimumSockTimeMinutes: pr.MinimumSoakTimeMinutes, + MinimumSuccessPercentage: pr.MinimumSuccessPercentage, + } + if pr.SuccessStatuses != nil { + statuses := make([]oapi.JobStatus, len(*pr.SuccessStatuses)) + for i, s := range *pr.SuccessStatuses { + statuses[i] = oapi.JobStatus(s) + } + rule.SuccessStatuses = &statuses + } + p.Rules = append(p.Rules, oapi.PolicyRule{ + Id: pr.Id, + PolicyId: p.Id, + EnvironmentProgression: &rule, + }) + } + + type rolloutJSON struct { + Id string `json:"id"` + RolloutType string `json:"rolloutType"` + TimeScaleInterval int32 `json:"timeScaleInterval"` + } + var rollouts []rolloutJSON + _ = json.Unmarshal(row.GradualRolloutRules, &rollouts) + for _, r := range rollouts { + p.Rules = append(p.Rules, oapi.PolicyRule{ + Id: r.Id, + PolicyId: p.Id, + GradualRollout: &oapi.GradualRolloutRule{ + RolloutType: oapi.GradualRolloutRuleRolloutType(r.RolloutType), + TimeScaleInterval: r.TimeScaleInterval, + }, + }) + } + + type cooldownJSON struct { + Id string `json:"id"` + IntervalSeconds int32 `json:"intervalSeconds"` + } + var cooldowns []cooldownJSON + _ = json.Unmarshal(row.VersionCooldownRules, &cooldowns) + for _, c := range cooldowns { + p.Rules = append(p.Rules, oapi.PolicyRule{ + Id: c.Id, + PolicyId: p.Id, + VersionCooldown: &oapi.VersionCooldownRule{ + IntervalSeconds: c.IntervalSeconds, + }, + }) + } + + type selectorJSON struct { + Id string `json:"id"` + Description *string `json:"description"` + Selector string `json:"selector"` + } + var selectors []selectorJSON + _ = json.Unmarshal(row.VersionSelectorRules, &selectors) + for _, s := range selectors { + var vSelector oapi.Selector + _ = json.Unmarshal([]byte(s.Selector), &vSelector) + p.Rules = append(p.Rules, oapi.PolicyRule{ + Id: s.Id, + PolicyId: p.Id, + VersionSelector: &oapi.VersionSelectorRule{ + Description: s.Description, + Selector: vSelector, + }, + }) + } + + return p +} + func ToOapiPolicy(row Policy) *oapi.Policy { p := &oapi.Policy{ Id: row.ID.String(), diff --git a/apps/workspace-engine/pkg/db/policies.sql.go b/apps/workspace-engine/pkg/db/policies.sql.go index 33832ab06..bbb503aa4 100644 --- a/apps/workspace-engine/pkg/db/policies.sql.go +++ b/apps/workspace-engine/pkg/db/policies.sql.go @@ -361,6 +361,77 @@ func (q *Queries) ListPoliciesByWorkspaceID(ctx context.Context, arg ListPolicie return items, nil } +const listPoliciesWithRulesByWorkspaceID = `-- name: ListPoliciesWithRulesByWorkspaceID :many +SELECT + p.id, p.name, p.description, p.selector, p.metadata, p.priority, p.enabled, p.workspace_id, p.created_at, + COALESCE((SELECT json_agg(json_build_object('id', r.id, 'minApprovals', r.min_approvals)) FROM policy_rule_any_approval r WHERE r.policy_id = p.id), '[]'::json) AS approval_rules, + COALESCE((SELECT json_agg(json_build_object('id', r.id, 'allowWindow', r.allow_window, 'durationMinutes', r.duration_minutes, 'rrule', r.rrule, 'timezone', r.timezone)) FROM policy_rule_deployment_window r WHERE r.policy_id = p.id), '[]'::json) AS deployment_window_rules, + COALESCE((SELECT json_agg(json_build_object('id', r.id, 'dependsOn', r.depends_on)) FROM policy_rule_deployment_dependency r WHERE r.policy_id = p.id), '[]'::json) AS deployment_dependency_rules, + COALESCE((SELECT json_agg(json_build_object('id', r.id, 'dependsOnEnvironmentSelector', r.depends_on_environment_selector, 'maximumAgeHours', r.maximum_age_hours, 'minimumSoakTimeMinutes', r.minimum_soak_time_minutes, 'minimumSuccessPercentage', r.minimum_success_percentage, 'successStatuses', r.success_statuses)) FROM policy_rule_environment_progression r WHERE r.policy_id = p.id), '[]'::json) AS environment_progression_rules, + COALESCE((SELECT json_agg(json_build_object('id', r.id, 'rolloutType', r.rollout_type, 'timeScaleInterval', r.time_scale_interval)) FROM policy_rule_gradual_rollout r WHERE r.policy_id = p.id), '[]'::json) AS gradual_rollout_rules, + COALESCE((SELECT json_agg(json_build_object('id', r.id, 'intervalSeconds', r.interval_seconds)) FROM policy_rule_version_cooldown r WHERE r.policy_id = p.id), '[]'::json) AS version_cooldown_rules, + COALESCE((SELECT json_agg(json_build_object('id', r.id, 'description', r.description, 'selector', r.selector)) FROM policy_rule_version_selector r WHERE r.policy_id = p.id), '[]'::json) AS version_selector_rules +FROM policy p +WHERE p.workspace_id = $1 +ORDER BY p.priority DESC, p.created_at DESC +` + +type ListPoliciesWithRulesByWorkspaceIDRow struct { + ID uuid.UUID + Name string + Description pgtype.Text + Selector string + Metadata map[string]string + Priority int32 + Enabled bool + WorkspaceID uuid.UUID + CreatedAt pgtype.Timestamptz + ApprovalRules []byte + DeploymentWindowRules []byte + DeploymentDependencyRules []byte + EnvironmentProgressionRules []byte + GradualRolloutRules []byte + VersionCooldownRules []byte + VersionSelectorRules []byte +} + +func (q *Queries) ListPoliciesWithRulesByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]ListPoliciesWithRulesByWorkspaceIDRow, error) { + rows, err := q.db.Query(ctx, listPoliciesWithRulesByWorkspaceID, workspaceID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListPoliciesWithRulesByWorkspaceIDRow + for rows.Next() { + var i ListPoliciesWithRulesByWorkspaceIDRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.Selector, + &i.Metadata, + &i.Priority, + &i.Enabled, + &i.WorkspaceID, + &i.CreatedAt, + &i.ApprovalRules, + &i.DeploymentWindowRules, + &i.DeploymentDependencyRules, + &i.EnvironmentProgressionRules, + &i.GradualRolloutRules, + &i.VersionCooldownRules, + &i.VersionSelectorRules, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const listRetryRulesByPolicyID = `-- name: ListRetryRulesByPolicyID :many SELECT id, policy_id, max_retries, backoff_seconds, backoff_strategy, diff --git a/apps/workspace-engine/pkg/db/queries/computed_resources.sql b/apps/workspace-engine/pkg/db/queries/computed_resources.sql index c424e9b68..36d25d87b 100644 --- a/apps/workspace-engine/pkg/db/queries/computed_resources.sql +++ b/apps/workspace-engine/pkg/db/queries/computed_resources.sql @@ -56,6 +56,23 @@ JOIN system_environment se AND se.system_id = sd.system_id WHERE cdr.resource_id = @resource_id; +-- name: GetReleaseTargetsForEnvironment :many +-- Returns all valid release targets for an environment by joining computed +-- resource tables through the system link tables. +SELECT DISTINCT + cdr.deployment_id, + cer.environment_id, + cdr.resource_id +FROM computed_deployment_resource cdr +JOIN computed_environment_resource cer + ON cer.resource_id = cdr.resource_id +JOIN system_deployment sd + ON sd.deployment_id = cdr.deployment_id +JOIN system_environment se + ON se.environment_id = cer.environment_id + AND se.system_id = sd.system_id +WHERE cer.environment_id = @environment_id; + -- name: GetReleaseTargetsForWorkspace :many -- Returns all valid release targets for a workspace. SELECT DISTINCT diff --git a/apps/workspace-engine/pkg/db/queries/policies.sql b/apps/workspace-engine/pkg/db/queries/policies.sql index 246991015..d662119b6 100644 --- a/apps/workspace-engine/pkg/db/queries/policies.sql +++ b/apps/workspace-engine/pkg/db/queries/policies.sql @@ -24,6 +24,20 @@ RETURNING *; -- name: DeletePolicy :exec DELETE FROM policy WHERE id = $1; +-- name: ListPoliciesWithRulesByWorkspaceID :many +SELECT + p.id, p.name, p.description, p.selector, p.metadata, p.priority, p.enabled, p.workspace_id, p.created_at, + COALESCE((SELECT json_agg(json_build_object('id', r.id, 'minApprovals', r.min_approvals)) FROM policy_rule_any_approval r WHERE r.policy_id = p.id), '[]'::json) AS approval_rules, + COALESCE((SELECT json_agg(json_build_object('id', r.id, 'allowWindow', r.allow_window, 'durationMinutes', r.duration_minutes, 'rrule', r.rrule, 'timezone', r.timezone)) FROM policy_rule_deployment_window r WHERE r.policy_id = p.id), '[]'::json) AS deployment_window_rules, + COALESCE((SELECT json_agg(json_build_object('id', r.id, 'dependsOn', r.depends_on)) FROM policy_rule_deployment_dependency r WHERE r.policy_id = p.id), '[]'::json) AS deployment_dependency_rules, + COALESCE((SELECT json_agg(json_build_object('id', r.id, 'dependsOnEnvironmentSelector', r.depends_on_environment_selector, 'maximumAgeHours', r.maximum_age_hours, 'minimumSoakTimeMinutes', r.minimum_soak_time_minutes, 'minimumSuccessPercentage', r.minimum_success_percentage, 'successStatuses', r.success_statuses)) FROM policy_rule_environment_progression r WHERE r.policy_id = p.id), '[]'::json) AS environment_progression_rules, + COALESCE((SELECT json_agg(json_build_object('id', r.id, 'rolloutType', r.rollout_type, 'timeScaleInterval', r.time_scale_interval)) FROM policy_rule_gradual_rollout r WHERE r.policy_id = p.id), '[]'::json) AS gradual_rollout_rules, + COALESCE((SELECT json_agg(json_build_object('id', r.id, 'intervalSeconds', r.interval_seconds)) FROM policy_rule_version_cooldown r WHERE r.policy_id = p.id), '[]'::json) AS version_cooldown_rules, + COALESCE((SELECT json_agg(json_build_object('id', r.id, 'description', r.description, 'selector', r.selector)) FROM policy_rule_version_selector r WHERE r.policy_id = p.id), '[]'::json) AS version_selector_rules +FROM policy p +WHERE p.workspace_id = $1 +ORDER BY p.priority DESC, p.created_at DESC; + -- ============================================================ -- policy_rule_any_approval -- ============================================================ diff --git a/apps/workspace-engine/svc/controllers/policysummary/controller.go b/apps/workspace-engine/svc/controllers/policysummary/controller.go index d1fd7e263..42a3454d4 100644 --- a/apps/workspace-engine/svc/controllers/policysummary/controller.go +++ b/apps/workspace-engine/svc/controllers/policysummary/controller.go @@ -13,7 +13,6 @@ import ( "workspace-engine/pkg/reconcile" "workspace-engine/pkg/reconcile/events" "workspace-engine/pkg/reconcile/postgres" - "workspace-engine/pkg/workspace/manager" "github.com/jackc/pgx/v5/pgxpool" "go.opentelemetry.io/otel" @@ -25,6 +24,7 @@ var tracer = otel.Tracer("workspace-engine/svc/controllers/policysummary") var _ reconcile.Processor = (*Controller)(nil) type Controller struct { + getter Getter setter Setter } @@ -37,14 +37,7 @@ func (c *Controller) Process(ctx context.Context, item reconcile.Item) (reconcil attribute.String("item.scope_id", item.ScopeID), ) - ws, ok := manager.Workspaces().Get(item.WorkspaceID) - if !ok { - return reconcile.Result{}, fmt.Errorf("workspace %s not found", item.WorkspaceID) - } - - getter := NewStoreGetter(ws) - - result, err := Reconcile(ctx, item.WorkspaceID, item.ScopeID, getter, c.setter) + result, err := Reconcile(ctx, item.WorkspaceID, item.ScopeID, c.getter, c.setter) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) @@ -59,6 +52,10 @@ func (c *Controller) Process(ctx context.Context, item reconcile.Item) (reconcil return reconcile.Result{}, nil } +func NewController(getter Getter, setter Setter) *Controller { + return &Controller{getter: getter, setter: setter} +} + func New(workerID string, pgxPool *pgxpool.Pool) svc.Service { if pgxPool == nil { log.Fatal("Failed to get pgx pool") @@ -83,6 +80,7 @@ func New(workerID string, pgxPool *pgxpool.Pool) svc.Service { queue := postgres.NewForKinds(pgxPool, kind) queries := db.New(pgxPool) controller := &Controller{ + getter: NewPostgresGetter(queries), setter: NewPostgresSetter(queries), } worker, err := reconcile.NewWorker( diff --git a/apps/workspace-engine/svc/controllers/policysummary/getters_postgres.go b/apps/workspace-engine/svc/controllers/policysummary/getters_postgres.go index 0190048d4..b38ee0de8 100644 --- a/apps/workspace-engine/svc/controllers/policysummary/getters_postgres.go +++ b/apps/workspace-engine/svc/controllers/policysummary/getters_postgres.go @@ -6,12 +6,17 @@ import ( "workspace-engine/pkg/db" "workspace-engine/pkg/oapi" + "workspace-engine/pkg/selector" "workspace-engine/svc/controllers/policysummary/summaryeval" "github.com/google/uuid" - "github.com/jackc/pgx/v5/pgtype" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" ) +var postgresGetterTracer = otel.Tracer("policysummary.getters_postgres") + type summaryevalGetter = summaryeval.PostgresGetter var _ Getter = (*PostgresGetter)(nil) @@ -37,31 +42,70 @@ func (g *PostgresGetter) GetVersion(ctx context.Context, versionID uuid.UUID) (* } func (g *PostgresGetter) GetPoliciesForEnvironment(ctx context.Context, workspaceID, environmentID uuid.UUID) ([]*oapi.Policy, error) { - rows, err := g.queries.ListPoliciesByWorkspaceID(ctx, db.ListPoliciesByWorkspaceIDParams{ - WorkspaceID: workspaceID, - Limit: pgtype.Int4{Int32: 5000, Valid: true}, - }) + ctx, span := postgresGetterTracer.Start(ctx, "GetPoliciesForEnvironment") + defer span.End() + + policyRows, err := g.queries.ListPoliciesWithRulesByWorkspaceID(ctx, workspaceID) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, "failed to list policies for workspace") return nil, fmt.Errorf("list policies for workspace %s: %w", workspaceID, err) } - policies := make([]*oapi.Policy, 0, len(rows)) - for _, row := range rows { - policies = append(policies, db.ToOapiPolicy(row)) + + span.SetAttributes(attribute.Int("policies_count", len(policyRows))) + + allPolicies := make([]*oapi.Policy, 0, len(policyRows)) + for _, row := range policyRows { + allPolicies = append(allPolicies, db.ToOapiPolicyWithRules(row)) } - return policies, nil -} -func (g *PostgresGetter) GetPoliciesForDeployment(ctx context.Context, workspaceID, deploymentID uuid.UUID) ([]*oapi.Policy, error) { - rows, err := g.queries.ListPoliciesByWorkspaceID(ctx, db.ListPoliciesByWorkspaceIDParams{ - WorkspaceID: workspaceID, - Limit: pgtype.Int4{Int32: 5000, Valid: true}, - }) + releaseTargets, err := g.queries.GetReleaseTargetsForEnvironment(ctx, environmentID) if err != nil { - return nil, fmt.Errorf("list policies for workspace %s: %w", workspaceID, err) + return nil, fmt.Errorf("get release targets for environment %s: %w", environmentID, err) + } + + span.SetAttributes(attribute.Int("release_targets_count", len(releaseTargets))) + + envRow, err := g.queries.GetEnvironmentByID(ctx, environmentID) + if err != nil { + return nil, fmt.Errorf("get environment %s: %w", environmentID, err) } - policies := make([]*oapi.Policy, 0, len(rows)) - for _, row := range rows { - policies = append(policies, db.ToOapiPolicy(row)) + environment := db.ToOapiEnvironment(envRow) + + span.SetAttributes(attribute.String("environment.id", environment.Id)) + + seen := make(map[string]struct{}) + var matched []*oapi.Policy + + for _, rt := range releaseTargets { + depRow, err := g.queries.GetDeploymentByID(ctx, rt.DeploymentID) + if err != nil { + continue + } + resRow, err := g.queries.GetResourceByID(ctx, rt.ResourceID) + if err != nil { + continue + } + deployment := db.ToOapiDeployment(depRow) + resource := db.ToOapiResource(resRow) + resolved := selector.NewResolvedReleaseTarget(environment, deployment, resource) + + for _, p := range allPolicies { + if _, ok := seen[p.Id]; ok { + continue + } + if selector.MatchPolicy(ctx, p, resolved) { + seen[p.Id] = struct{}{} + matched = append(matched, p) + } + } } - return policies, nil + + span.SetAttributes(attribute.Int("matched_policies_count", len(matched))) + + return matched, nil +} + +func (g *PostgresGetter) GetPoliciesForDeployment(ctx context.Context, workspaceID, deploymentID uuid.UUID) ([]*oapi.Policy, error) { + return g.GetPoliciesForEnvironment(ctx, workspaceID, deploymentID) } From 2a0619845aba01ee717f2182ee7c38296de1124a Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Sat, 7 Mar 2026 08:38:47 -0800 Subject: [PATCH 11/11] lint --- apps/workspace-engine/pkg/db/convert.go | 16 ++-- .../policysummary/getters_store.go | 82 ------------------- 2 files changed, 6 insertions(+), 92 deletions(-) delete mode 100644 apps/workspace-engine/svc/controllers/policysummary/getters_store.go diff --git a/apps/workspace-engine/pkg/db/convert.go b/apps/workspace-engine/pkg/db/convert.go index 7916e6d71..3770af7e7 100644 --- a/apps/workspace-engine/pkg/db/convert.go +++ b/apps/workspace-engine/pkg/db/convert.go @@ -82,10 +82,6 @@ func ToOapiPolicyWithRules(row ListPoliciesWithRulesByWorkspaceIDRow) *oapi.Poli CreatedAt: row.CreatedAt, }) - type ruleID struct { - Id string `json:"id"` - } - type approvalJSON struct { Id string `json:"id"` MinApprovals int32 `json:"minApprovals"` @@ -141,12 +137,12 @@ func ToOapiPolicyWithRules(row ListPoliciesWithRulesByWorkspaceIDRow) *oapi.Poli } type progressionJSON struct { - Id string `json:"id"` - DependsOnEnvironmentSelector string `json:"dependsOnEnvironmentSelector"` - MaximumAgeHours *int32 `json:"maximumAgeHours"` - MinimumSoakTimeMinutes *int32 `json:"minimumSoakTimeMinutes"` - MinimumSuccessPercentage *float32 `json:"minimumSuccessPercentage"` - SuccessStatuses *[]string `json:"successStatuses"` + Id string `json:"id"` + DependsOnEnvironmentSelector string `json:"dependsOnEnvironmentSelector"` + MaximumAgeHours *int32 `json:"maximumAgeHours"` + MinimumSoakTimeMinutes *int32 `json:"minimumSoakTimeMinutes"` + MinimumSuccessPercentage *float32 `json:"minimumSuccessPercentage"` + SuccessStatuses *[]string `json:"successStatuses"` } var progs []progressionJSON _ = json.Unmarshal(row.EnvironmentProgressionRules, &progs) diff --git a/apps/workspace-engine/svc/controllers/policysummary/getters_store.go b/apps/workspace-engine/svc/controllers/policysummary/getters_store.go deleted file mode 100644 index bffa561a2..000000000 --- a/apps/workspace-engine/svc/controllers/policysummary/getters_store.go +++ /dev/null @@ -1,82 +0,0 @@ -package policysummary - -import ( - "context" - "fmt" - - "workspace-engine/pkg/oapi" - "workspace-engine/pkg/workspace" - "workspace-engine/svc/controllers/policysummary/summaryeval" - - "github.com/google/uuid" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/trace" -) - -var storeGetterTracer = otel.Tracer("policysummary.getters_store") - -type storeEvalGetter = summaryeval.StoreGetter - -var _ Getter = (*StoreGetter)(nil) - -type StoreGetter struct { - *storeEvalGetter - ws *workspace.Workspace -} - -func NewStoreGetter(ws *workspace.Workspace) *StoreGetter { - return &StoreGetter{ - storeEvalGetter: summaryeval.NewStoreGetter(ws.Store()), - ws: ws, - } -} - -func (g *StoreGetter) GetVersion(ctx context.Context, versionID uuid.UUID) (*oapi.DeploymentVersion, error) { - ver, ok := g.ws.DeploymentVersions().Get(versionID.String()) - if !ok { - return nil, fmt.Errorf("version %s not found", versionID) - } - return ver, nil -} - -func (g *StoreGetter) GetPoliciesForEnvironment(ctx context.Context, workspaceID, environmentID uuid.UUID) ([]*oapi.Policy, error) { - ctx, span := storeGetterTracer.Start(ctx, "GetPoliciesForEnvironment") - defer span.End() - - releaseTargets, err := g.ws.ReleaseTargets().GetForEnvironment(ctx, environmentID.String()) - if err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, "failed to get release targets for environment") - return nil, fmt.Errorf("get release targets for environment: %w", err) - } - - span.SetAttributes(attribute.Int("release_targets_count", len(releaseTargets))) - - allPolicies := make(map[string]*oapi.Policy) - - for _, rt := range releaseTargets { - policies, err := g.storeEvalGetter.GetPoliciesForReleaseTarget(ctx, rt) - span.AddEvent("get_policies_for_release_target", trace.WithAttributes(attribute.Int("policies_count", len(policies)))) - if err != nil { - return nil, fmt.Errorf("get policies for release target: %w", err) - } - for _, p := range policies { - allPolicies[p.Id] = p - } - } - - policiesSlice := make([]*oapi.Policy, 0, len(allPolicies)) - for _, p := range allPolicies { - policiesSlice = append(policiesSlice, p) - } - - span.SetAttributes(attribute.Int("policies_count", len(policiesSlice))) - - return policiesSlice, nil -} - -func (g *StoreGetter) GetPoliciesForDeployment(ctx context.Context, workspaceID, deploymentID uuid.UUID) ([]*oapi.Policy, error) { - return g.GetPoliciesForEnvironment(ctx, workspaceID, deploymentID) -}