From bb23f749b14f2509fabd12437a0cccafc64a4c3c Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 6 Mar 2026 02:07:41 -0500 Subject: [PATCH 01/10] refactor: update DeploymentDependencyEvaluator to use getters for data access and rename evaluator constructor --- .../deployment_dependency.go | 28 +++++++++++------ .../deployment_dependency_test.go | 16 +++++----- .../evaluator/deploymentdependency/getters.go | 31 +++++++++++++++++++ .../releasemanager/policy/evaluators.go | 2 +- .../releasemanager/policy/policymanager.go | 2 +- .../desiredrelease/policyeval/policyeval.go | 20 +++++++++--- 6 files changed, 74 insertions(+), 25 deletions(-) create mode 100644 apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentdependency/getters.go diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentdependency/deployment_dependency.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentdependency/deployment_dependency.go index 68baadc81..d8af9faa5 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentdependency/deployment_dependency.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentdependency/deployment_dependency.go @@ -22,20 +22,28 @@ func celToSelector(cel string) *oapi.Selector { var tracer = otel.Tracer("DeploymentDependencyEvaluator") type DeploymentDependencyEvaluator struct { - store *store.Store - ruleId string - rule *oapi.DeploymentDependencyRule + getters Getters + ruleId string + rule *oapi.DeploymentDependencyRule } -func NewEvaluator(store *store.Store, dependencyRule *oapi.PolicyRule) evaluator.Evaluator { +func NewEvaluatorFromStore(store *store.Store, dependencyRule *oapi.PolicyRule) evaluator.Evaluator { if dependencyRule == nil || dependencyRule.DeploymentDependency == nil || store == nil { return nil } + return NewEvaluator(&storeGetters{store: store}, dependencyRule) +} + +func NewEvaluator(getters Getters, dependencyRule *oapi.PolicyRule) evaluator.Evaluator { + if dependencyRule == nil || dependencyRule.DeploymentDependency == nil || getters == nil { + return nil + } + return evaluator.WithMemoization(&DeploymentDependencyEvaluator{ - store: store, - ruleId: dependencyRule.Id, - rule: dependencyRule.DeploymentDependency, + getters: getters, + ruleId: dependencyRule.Id, + rule: dependencyRule.DeploymentDependency, }) } @@ -58,7 +66,7 @@ func (e *DeploymentDependencyEvaluator) Complexity() int { func (e *DeploymentDependencyEvaluator) findMatchingDeployments(ctx context.Context) ([]*oapi.Deployment, error) { deploymentSelector := celToSelector(e.rule.DependsOn) matchingDeployments := make([]*oapi.Deployment, 0) - for _, deployment := range e.store.Deployments.Items() { + for _, deployment := range e.getters.GetDeployments() { matched, err := selector.Match(ctx, deploymentSelector, deployment) if err != nil { return nil, fmt.Errorf("failed to match deployment selector: %w", err) @@ -72,7 +80,7 @@ func (e *DeploymentDependencyEvaluator) findMatchingDeployments(ctx context.Cont func (e *DeploymentDependencyEvaluator) getUpstreamReleaseTargets(ctx context.Context, matchingDeployments []*oapi.Deployment, resourceID string) []*oapi.ReleaseTarget { upstreamReleaseTargets := make([]*oapi.ReleaseTarget, 0, len(matchingDeployments)) - resourceTargets := e.store.ReleaseTargets.GetForResource(ctx, resourceID) + resourceTargets := e.getters.GetReleaseTargetsForResource(ctx, resourceID) deploymentToTargetMap := make(map[string]*oapi.ReleaseTarget) for _, resourceTarget := range resourceTargets { @@ -89,7 +97,7 @@ func (e *DeploymentDependencyEvaluator) getUpstreamReleaseTargets(ctx context.Co } func (e *DeploymentDependencyEvaluator) checkUpstreamTargetHasSuccessfulRelease(upstreamReleaseTarget *oapi.ReleaseTarget) bool { - latestJob := e.store.Jobs.GetLatestCompletedJobForReleaseTarget(upstreamReleaseTarget) + latestJob := e.getters.GetLatestCompletedJobForReleaseTarget(upstreamReleaseTarget) if latestJob == nil { return false } diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentdependency/deployment_dependency_test.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentdependency/deployment_dependency_test.go index a4f2d7926..3437309b5 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentdependency/deployment_dependency_test.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentdependency/deployment_dependency_test.go @@ -122,7 +122,7 @@ func TestDeploymentDependencyEvaluator_UnsatisfiedDependencyFails(t *testing.T) cel := fmt.Sprintf("deployment.id == '%s'", deployment1.Id) rule := generateDependencyRule(cel) - eval := NewEvaluator(st, rule) + eval := NewEvaluatorFromStore(st, rule) result := eval.Evaluate(ctx, evaluator.EvaluatorScope{ Environment: &oapi.Environment{Id: releaseTarget.EnvironmentId}, Resource: &oapi.Resource{Id: releaseTarget.ResourceId}, @@ -154,7 +154,7 @@ func TestDeploymentDependencyEvaluator_SatisfiedDependencyPasses(t *testing.T) { cel := fmt.Sprintf("deployment.id == '%s'", deployment1.Id) rule := generateDependencyRule(cel) - eval := NewEvaluator(st, rule) + eval := NewEvaluatorFromStore(st, rule) result := eval.Evaluate(ctx, evaluator.EvaluatorScope{ Environment: &oapi.Environment{Id: releaseTarget2.EnvironmentId}, Resource: &oapi.Resource{Id: releaseTarget2.ResourceId}, @@ -193,7 +193,7 @@ func TestDeploymentDependencyEvaluator_MixedSatisfactionsFails(t *testing.T) { cel := fmt.Sprintf("deployment.id != '%s'", deployment3.Id) rule := generateDependencyRule(cel) - eval := NewEvaluator(st, rule) + eval := NewEvaluatorFromStore(st, rule) result := eval.Evaluate(ctx, evaluator.EvaluatorScope{ Environment: &oapi.Environment{Id: releaseTarget3.EnvironmentId}, @@ -233,7 +233,7 @@ func TestDeploymentDependencyEvaluator_FailedJobsFails(t *testing.T) { cel := fmt.Sprintf("deployment.id != '%s'", deployment3.Id) rule := generateDependencyRule(cel) - eval := NewEvaluator(st, rule) + eval := NewEvaluatorFromStore(st, rule) result := eval.Evaluate(ctx, evaluator.EvaluatorScope{ Environment: &oapi.Environment{Id: releaseTarget3.EnvironmentId}, @@ -273,7 +273,7 @@ func TestDeploymentDependencyEvaluator_FailsIfLatestJobIsNotSuccessful(t *testin cel := fmt.Sprintf("deployment.id == '%s'", deployment1.Id) rule := generateDependencyRule(cel) - eval := NewEvaluator(st, rule) + eval := NewEvaluatorFromStore(st, rule) result := eval.Evaluate(ctx, evaluator.EvaluatorScope{ Environment: &oapi.Environment{Id: releaseTarget2.EnvironmentId}, Resource: &oapi.Resource{Id: releaseTarget2.ResourceId}, @@ -312,7 +312,7 @@ func TestDeploymentDependencyEvaluator_PassesIfLatestJobIsProgressingAndOtherJob cel := fmt.Sprintf("deployment.id == '%s'", deployment1.Id) rule := generateDependencyRule(cel) - eval := NewEvaluator(st, rule) + eval := NewEvaluatorFromStore(st, rule) result := eval.Evaluate(ctx, evaluator.EvaluatorScope{ Environment: &oapi.Environment{Id: releaseTarget2.EnvironmentId}, Resource: &oapi.Resource{Id: releaseTarget2.ResourceId}, @@ -343,7 +343,7 @@ func TestDeploymentDependencyEvaluator_NoMatchingDeploymentsFails(t *testing.T) cel := "deployment.id == 'non-existing-deployment'" rule := generateDependencyRule(cel) - eval := NewEvaluator(st, rule) + eval := NewEvaluatorFromStore(st, rule) result := eval.Evaluate(ctx, evaluator.EvaluatorScope{ Environment: &oapi.Environment{Id: releaseTarget2.EnvironmentId}, @@ -374,7 +374,7 @@ func TestDeploymentDependencyEvaluator_NotEnoughUpstreamReleaseTargetsFails(t *t cel := fmt.Sprintf("deployment.id == '%s'", deployment1.Id) rule := generateDependencyRule(cel) - eval := NewEvaluator(st, rule) + eval := NewEvaluatorFromStore(st, rule) result := eval.Evaluate(ctx, evaluator.EvaluatorScope{ Environment: &oapi.Environment{Id: releaseTarget2.EnvironmentId}, diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentdependency/getters.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentdependency/getters.go new file mode 100644 index 000000000..7b22a76ef --- /dev/null +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentdependency/getters.go @@ -0,0 +1,31 @@ +package deploymentdependency + +import ( + "context" + "workspace-engine/pkg/oapi" + "workspace-engine/pkg/workspace/store" +) + +type Getters interface { + GetDeployments() map[string]*oapi.Deployment + GetReleaseTargetsForResource(ctx context.Context, resourceID string) []*oapi.ReleaseTarget + GetLatestCompletedJobForReleaseTarget(releaseTarget *oapi.ReleaseTarget) *oapi.Job +} + +var _ Getters = (*storeGetters)(nil) + +type storeGetters struct { + store *store.Store +} + +func (s *storeGetters) GetDeployments() map[string]*oapi.Deployment { + return s.store.Deployments.Items() +} + +func (s *storeGetters) GetReleaseTargetsForResource(ctx context.Context, resourceID string) []*oapi.ReleaseTarget { + return s.store.ReleaseTargets.GetForResource(ctx, resourceID) +} + +func (s *storeGetters) GetLatestCompletedJobForReleaseTarget(releaseTarget *oapi.ReleaseTarget) *oapi.Job { + return s.store.Jobs.GetLatestCompletedJobForReleaseTarget(releaseTarget) +} diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluators.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluators.go index 81577024a..db2179fe6 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluators.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluators.go @@ -21,7 +21,7 @@ func EvaluatorsForPolicy(store *store.Store, rule *oapi.PolicyRule) []evaluator. environmentprogression.NewEvaluatorFromStore(store, rule), gradualrollout.NewEvaluatorFromStore(store, rule), versionselector.NewEvaluator(rule), - deploymentdependency.NewEvaluator(store, rule), + deploymentdependency.NewEvaluatorFromStore(store, rule), deploymentwindow.NewEvaluatorFromStore(store, rule), versioncooldown.NewEvaluatorFromStore(store, rule), ) diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/policymanager.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/policymanager.go index d42d86bde..96221c3f6 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/policymanager.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/policymanager.go @@ -46,7 +46,7 @@ func (m *Manager) PlannerPolicyEvaluators(rule *oapi.PolicyRule) []evaluator.Eva environmentprogression.NewEvaluatorFromStore(m.store, rule), gradualrollout.NewEvaluatorFromStore(m.store, rule), versionselector.NewEvaluator(rule), - deploymentdependency.NewEvaluator(m.store, rule), + deploymentdependency.NewEvaluatorFromStore(m.store, rule), deploymentwindow.NewEvaluatorFromStore(m.store, rule), versioncooldown.NewEvaluatorFromStore(m.store, rule), ) diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval.go b/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval.go index ee30a71a1..2050257ec 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval.go @@ -11,23 +11,33 @@ import ( "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/approval" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deployableversions" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentwindow" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versioncooldown" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector" ) // Getter provides the data-access methods needed by policy evaluators. type Getter interface { - GetApprovalRecords(ctx context.Context, versionID, environmentID string) ([]*oapi.UserApprovalRecord, error) - HasCurrentRelease(ctx context.Context, rt *oapi.ReleaseTarget) (bool, error) - GetCurrentRelease(ctx context.Context, rt *oapi.ReleaseTarget) (*oapi.Release, error) + approval.Getters + environmentprogression.Getters + deploymentwindow.Getters + // deploymentdependency.Getters + versioncooldown.Getters + deployableversions.Getters + GetPolicySkips(ctx context.Context, versionID, environmentID, resourceID string) ([]*oapi.PolicySkip, error) } // ruleEvaluators returns evaluators for a single policy rule. func ruleEvaluators(ctx context.Context, getter Getter, rule *oapi.PolicyRule) []evaluator.Evaluator { return evaluator.CollectEvaluators( + approval.NewEvaluator(getter, rule), + environmentprogression.NewEvaluator(getter, rule), + // gradualrollout.NewEvaluator(getter, rule), versionselector.NewEvaluator(rule), - approval.NewEvaluator(&approvalAdapter{getter: getter, ctx: ctx}, rule), - deploymentwindow.NewEvaluator(&deploymentWindowAdapter{getter: getter, ctx: ctx}, rule), + // deploymentdependency.NewEvaluator(getter, rule), + deploymentwindow.NewEvaluator(getter, rule), + versioncooldown.NewEvaluator(getter, rule), ) } From b916b217f167e5335b1799ee42c5b1d76975067a Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 6 Mar 2026 02:08:36 -0500 Subject: [PATCH 02/10] refactor: integrate DeploymentDependencyEvaluator into policy evaluation process --- .../svc/controllers/desiredrelease/policyeval/policyeval.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval.go b/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval.go index 2050257ec..c43dc35a4 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval.go @@ -10,6 +10,7 @@ import ( "workspace-engine/pkg/workspace/releasemanager/policy/evaluator" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/approval" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deployableversions" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentdependency" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentwindow" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versioncooldown" @@ -21,7 +22,7 @@ type Getter interface { approval.Getters environmentprogression.Getters deploymentwindow.Getters - // deploymentdependency.Getters + deploymentdependency.Getters versioncooldown.Getters deployableversions.Getters @@ -35,7 +36,7 @@ func ruleEvaluators(ctx context.Context, getter Getter, rule *oapi.PolicyRule) [ environmentprogression.NewEvaluator(getter, rule), // gradualrollout.NewEvaluator(getter, rule), versionselector.NewEvaluator(rule), - // deploymentdependency.NewEvaluator(getter, rule), + deploymentdependency.NewEvaluator(getter, rule), deploymentwindow.NewEvaluator(getter, rule), versioncooldown.NewEvaluator(getter, rule), ) From 6e51dbed808cc887f458f4ea68161533a05ba6b3 Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 6 Mar 2026 03:11:47 -0500 Subject: [PATCH 03/10] refactor: streamline policy evaluation by removing mockLoader and integrating direct candidate access --- .../workspace/relationships/eval/evaluate.go | 44 +--- .../relationships/eval/evaluate_test.go | 207 +----------------- .../policy/evaluator/approval/approval.go | 16 +- .../environment_progression_action.go | 41 ++-- .../environmentprogression/getters.go | 25 --- .../pkg/workspace/workspace.go | 2 +- .../controllers/desiredrelease/adapters.go | 50 ----- .../svc/controllers/desiredrelease/getters.go | 20 +- .../desiredrelease/getters_postgres.go | 4 +- .../desiredrelease/policyeval/adapters.go | 50 ----- .../desiredrelease/policyeval/policyeval.go | 2 +- .../controllers/desiredrelease/reconcile.go | 10 +- .../variableresolver/related_resolver.go | 89 ++++++++ .../variableresolver/resolve.go | 101 +-------- .../desiredrelease/variableresolver/utils.go | 101 +++++++++ .../desiredrelease/variableresolver/value.go | 10 +- 16 files changed, 259 insertions(+), 513 deletions(-) delete mode 100644 apps/workspace-engine/svc/controllers/desiredrelease/policyeval/adapters.go create mode 100644 apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/related_resolver.go create mode 100644 apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/utils.go diff --git a/apps/workspace-engine/pkg/workspace/relationships/eval/evaluate.go b/apps/workspace-engine/pkg/workspace/relationships/eval/evaluate.go index 1a0dfb7d5..9f1edf497 100644 --- a/apps/workspace-engine/pkg/workspace/relationships/eval/evaluate.go +++ b/apps/workspace-engine/pkg/workspace/relationships/eval/evaluate.go @@ -119,27 +119,18 @@ func EvaluateRule( // candidates via the provided loader. Returns all matches across all rules. func EvaluateRules( ctx context.Context, - loader CandidateLoader, entity *EntityData, + candidates []EntityData, rules []Rule, ) ([]Match, error) { ctx, span := tracer.Start(ctx, "eval.EvaluateRules") defer span.End() - var allCandidates []EntityData - for _, entityType := range knownEntityTypes { - candidates, err := loader.LoadCandidates(ctx, entity.WorkspaceID, entityType) - if err != nil { - return nil, fmt.Errorf("load candidates for entity %s (type %s): %w", entity.ID, entityType, err) - } - allCandidates = append(allCandidates, candidates...) - } - var allMatches []Match for i := range rules { rule := &rules[i] - matches, err := EvaluateRule(ctx, entity, rule, allCandidates) + matches, err := EvaluateRule(ctx, entity, rule, candidates) if err != nil { return nil, fmt.Errorf("evaluate rule %s: %w", rule.ID, err) } @@ -149,34 +140,3 @@ func EvaluateRules( span.SetAttributes(attribute.Int("matches.total", len(allMatches))) return allMatches, nil } - -// ResolveForReference finds all entities related to entity through rules -// matching the given reference name. Only rules whose Reference field -// matches are evaluated, keeping the candidate search targeted. -func ResolveForReference( - ctx context.Context, - loader CandidateLoader, - entity *EntityData, - rules []Rule, - reference string, -) ([]Match, error) { - ctx, span := tracer.Start(ctx, "eval.ResolveForReference") - defer span.End() - span.SetAttributes( - attribute.String("reference", reference), - attribute.String("entity.id", entity.ID.String()), - ) - - filtered := make([]Rule, 0, len(rules)) - for _, r := range rules { - if r.Reference == reference { - filtered = append(filtered, r) - } - } - - if len(filtered) == 0 { - return nil, nil - } - - return EvaluateRules(ctx, loader, entity, filtered) -} diff --git a/apps/workspace-engine/pkg/workspace/relationships/eval/evaluate_test.go b/apps/workspace-engine/pkg/workspace/relationships/eval/evaluate_test.go index 0f74bfe03..fb8263677 100644 --- a/apps/workspace-engine/pkg/workspace/relationships/eval/evaluate_test.go +++ b/apps/workspace-engine/pkg/workspace/relationships/eval/evaluate_test.go @@ -55,19 +55,6 @@ func nameMatchRule(ruleID uuid.UUID, ref, fromType, toType string) Rule { return Rule{ID: ruleID, Reference: ref, Cel: cel} } -// mockLoader returns pre-configured candidates by entity type. -type mockLoader struct { - candidates map[string][]EntityData - err error -} - -func (m *mockLoader) LoadCandidates(_ context.Context, _ uuid.UUID, entityType string) ([]EntityData, error) { - if m.err != nil { - return nil, m.err - } - return m.candidates[entityType], nil -} - // sortMatches provides deterministic ordering for assertions. func sortMatches(ms []Match) { sort.Slice(ms, func(i, j int) bool { @@ -314,11 +301,9 @@ func TestEvaluateRules_MultipleRules(t *testing.T) { entity := resourceEntity(entityID, wsID, "web", "Server", nil) - loader := &mockLoader{ - candidates: map[string][]EntityData{ - "deployment": {deploymentEntity(depID, wsID, "web", "web", nil)}, - "environment": {environmentEntity(envID, wsID, "web")}, - }, + candidates := []EntityData{ + deploymentEntity(depID, wsID, "web", "web", nil), + environmentEntity(envID, wsID, "web"), } rules := []Rule{ @@ -326,7 +311,7 @@ func TestEvaluateRules_MultipleRules(t *testing.T) { nameMatchRule(newID(), "env", "resource", "environment"), } - matches, err := EvaluateRules(context.Background(), loader, &entity, rules) + matches, err := EvaluateRules(context.Background(), &entity, candidates, rules) require.NoError(t, err) require.Len(t, matches, 2) @@ -342,210 +327,40 @@ func TestEvaluateRules_SkipsIrrelevantRules(t *testing.T) { wsID := newID() entity := environmentEntity(newID(), wsID, "prod") - loader := &mockLoader{ - candidates: map[string][]EntityData{ - "deployment": {deploymentEntity(newID(), wsID, "prod", "prod", nil)}, - }, + candidates := []EntityData{ + deploymentEntity(newID(), wsID, "prod", "prod", nil), } rules := []Rule{ nameMatchRule(newID(), "dep", "resource", "deployment"), } - matches, err := EvaluateRules(context.Background(), loader, &entity, rules) + matches, err := EvaluateRules(context.Background(), &entity, candidates, rules) require.NoError(t, err) assert.Empty(t, matches) } func TestEvaluateRules_EmptyRules(t *testing.T) { entity := resourceEntity(newID(), newID(), "web", "Server", nil) - loader := &mockLoader{} - matches, err := EvaluateRules(context.Background(), loader, &entity, nil) + matches, err := EvaluateRules(context.Background(), &entity, nil, nil) require.NoError(t, err) assert.Empty(t, matches) } -func TestEvaluateRules_LoaderError(t *testing.T) { - wsID := newID() - entity := resourceEntity(newID(), wsID, "web", "Server", nil) - - loader := &mockLoader{err: fmt.Errorf("db connection failed")} - rules := []Rule{nameMatchRule(newID(), "dep", "resource", "deployment")} - - _, err := EvaluateRules(context.Background(), loader, &entity, rules) - require.Error(t, err) - assert.Contains(t, err.Error(), "load candidates") - assert.Contains(t, err.Error(), "db connection failed") -} - func TestEvaluateRules_CELErrorPropagated(t *testing.T) { wsID := newID() entity := resourceEntity(newID(), wsID, "web", "Server", nil) - loader := &mockLoader{ - candidates: map[string][]EntityData{ - "deployment": {deploymentEntity(newID(), wsID, "web", "web", nil)}, - }, + candidates := []EntityData{ + deploymentEntity(newID(), wsID, "web", "web", nil), } rules := []Rule{{ ID: newID(), Reference: "bad", Cel: "not valid!!!", }} - _, err := EvaluateRules(context.Background(), loader, &entity, rules) + _, err := EvaluateRules(context.Background(), &entity, candidates, rules) require.Error(t, err) assert.Contains(t, err.Error(), "evaluate rule") } - -// --------------------------------------------------------------------------- -// ResolveForReference -// --------------------------------------------------------------------------- - -func TestResolveForReference_FiltersToMatchingReference(t *testing.T) { - wsID := newID() - entityID := newID() - dbID := newID() - cacheID := newID() - - entity := resourceEntity(entityID, wsID, "web", "Server", nil) - - loader := &mockLoader{ - candidates: map[string][]EntityData{ - "resource": { - resourceEntity(dbID, wsID, "web", "Database", nil), - resourceEntity(cacheID, wsID, "web", "Cache", nil), - }, - }, - } - - dbRuleID := newID() - cacheRuleID := newID() - - rules := []Rule{ - { - ID: dbRuleID, Reference: "database", - Cel: `to.kind == "Database"`, - }, - { - ID: cacheRuleID, Reference: "cache", - Cel: `to.kind == "Cache"`, - }, - } - - matches, err := ResolveForReference(context.Background(), loader, &entity, rules, "database") - require.NoError(t, err) - require.Len(t, matches, 1) - assert.Equal(t, dbRuleID, matches[0].RuleID) - assert.Equal(t, "database", matches[0].Reference) - assert.Equal(t, dbID, matches[0].ToEntityID) -} - -func TestResolveForReference_NoMatchingReference(t *testing.T) { - entity := resourceEntity(newID(), newID(), "web", "Server", nil) - loader := &mockLoader{} - rules := []Rule{nameMatchRule(newID(), "dep", "resource", "deployment")} - - matches, err := ResolveForReference(context.Background(), loader, &entity, rules, "nonexistent") - require.NoError(t, err) - assert.Nil(t, matches) -} - -func TestResolveForReference_EmptyRules(t *testing.T) { - entity := resourceEntity(newID(), newID(), "web", "Server", nil) - loader := &mockLoader{} - - matches, err := ResolveForReference(context.Background(), loader, &entity, nil, "anything") - require.NoError(t, err) - assert.Nil(t, matches) -} - -func TestResolveForReference_MultipleRulesSameReference(t *testing.T) { - wsID := newID() - entityID := newID() - c1 := newID() - c2 := newID() - - entity := resourceEntity(entityID, wsID, "web", "Server", nil) - - loader := &mockLoader{ - candidates: map[string][]EntityData{ - "resource": { - resourceEntity(c1, wsID, "db-primary", "Database", nil), - resourceEntity(c2, wsID, "db-replica", "Database", nil), - }, - }, - } - - rules := []Rule{ - { - ID: newID(), Reference: "database", - Cel: `to.name == "db-primary"`, - }, - { - ID: newID(), Reference: "database", - Cel: `to.name == "db-replica"`, - }, - } - - matches, err := ResolveForReference(context.Background(), loader, &entity, rules, "database") - require.NoError(t, err) - require.Len(t, matches, 2) - - matchedIDs := map[uuid.UUID]bool{} - for _, m := range matches { - matchedIDs[m.ToEntityID] = true - } - assert.True(t, matchedIDs[c1]) - assert.True(t, matchedIDs[c2]) -} - -func TestResolveForReference_CrossTypeRule(t *testing.T) { - wsID := newID() - entityID := newID() - depID := newID() - - entity := resourceEntity(entityID, wsID, "api", "Server", nil) - - loader := &mockLoader{ - candidates: map[string][]EntityData{ - "deployment": {deploymentEntity(depID, wsID, "api", "api", nil)}, - }, - } - - rules := []Rule{ - nameMatchRule(newID(), "deploy", "resource", "deployment"), - } - - matches, err := ResolveForReference(context.Background(), loader, &entity, rules, "deploy") - require.NoError(t, err) - require.Len(t, matches, 1) - assert.Equal(t, entityID, matches[0].FromEntityID) - assert.Equal(t, depID, matches[0].ToEntityID) -} - -func TestResolveForReference_ReverseDirection(t *testing.T) { - wsID := newID() - entityID := newID() - resID := newID() - - entity := deploymentEntity(entityID, wsID, "api", "api", nil) - - loader := &mockLoader{ - candidates: map[string][]EntityData{ - "resource": {resourceEntity(resID, wsID, "api", "Server", nil)}, - }, - } - - rules := []Rule{ - nameMatchRule(newID(), "res", "resource", "deployment"), - } - - matches, err := ResolveForReference(context.Background(), loader, &entity, rules, "res") - require.NoError(t, err) - require.Len(t, matches, 1) - assert.Equal(t, resID, matches[0].FromEntityID) - assert.Equal(t, "resource", matches[0].FromEntityType) - assert.Equal(t, entityID, matches[0].ToEntityID) - assert.Equal(t, "deployment", matches[0].ToEntityType) -} diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/approval/approval.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/approval/approval.go index f23f646a0..f7794c3a5 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/approval/approval.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/approval/approval.go @@ -42,7 +42,7 @@ func parseTimestamp(s string) (time.Time, error) { var _ evaluator.Evaluator = &AnyApprovalEvaluator{} type Getters interface { - GetApprovalRecords(versionID, environmentID string) []*oapi.UserApprovalRecord + GetApprovalRecords(ctx context.Context,versionID, environmentID string) ([]*oapi.UserApprovalRecord, error) } type AnyApprovalEvaluator struct { @@ -56,8 +56,8 @@ type storeGetters struct { store *store.Store } -func (s *storeGetters) GetApprovalRecords(versionID, environmentID string) []*oapi.UserApprovalRecord { - return s.store.UserApprovalRecords.GetApprovalRecords(versionID, environmentID) +func (s *storeGetters) GetApprovalRecords(ctx context.Context, versionID, environmentID string) ([]*oapi.UserApprovalRecord, error) { + return s.store.UserApprovalRecords.GetApprovalRecords(versionID, environmentID), nil } func NewEvaluatorFromStore(store *store.Store, approvalRule *oapi.PolicyRule) evaluator.Evaluator { @@ -120,7 +120,15 @@ func (m *AnyApprovalEvaluator) Evaluate( WithSatisfiedAt(version.CreatedAt) } - approvalRecords := m.getters.GetApprovalRecords(version.Id, environment.Id) + approvalRecords, err := m.getters.GetApprovalRecords(ctx, version.Id, environment.Id) + if err != nil { + return results. + NewPendingResult("approval", + fmt.Sprintf("Failed to get approval records: %v", err), + ). + WithDetail("version_id", version.Id). + WithDetail("environment_id", environment.Id) + } minApprovals := int(m.rule.MinApprovals) approvers := make([]string, len(approvalRecords)) diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environment_progression_action.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environment_progression_action.go index c32ab4934..9982b6524 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environment_progression_action.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environment_progression_action.go @@ -19,17 +19,13 @@ var actionTracer = otel.Tracer("EnvironmentProgressionAction") type ReconcileFn func(ctx context.Context, targets []*oapi.ReleaseTarget) error type EnvironmentProgressionAction struct { - getters Getters + store *store.Store reconcileFn ReconcileFn } -func NewEnvironmentProgressionActionFromStore(store *store.Store, reconcileFn ReconcileFn) *EnvironmentProgressionAction { - return NewEnvironmentProgressionAction(&storeGetters{store: store}, reconcileFn) -} - -func NewEnvironmentProgressionAction(getters Getters, reconcileFn ReconcileFn) *EnvironmentProgressionAction { +func NewEnvironmentProgressionAction(store *store.Store, reconcileFn ReconcileFn) *EnvironmentProgressionAction { return &EnvironmentProgressionAction{ - getters: getters, + store: store, reconcileFn: reconcileFn, } } @@ -53,8 +49,8 @@ func (a *EnvironmentProgressionAction) Execute(ctx context.Context, trigger acti return nil } - environment := a.getEnvironment(actx.Release.ReleaseTarget.EnvironmentId) - if environment == nil { + environment, ok := a.store.Environments.Get(actx.Release.ReleaseTarget.EnvironmentId) + if !ok { return nil } @@ -68,8 +64,9 @@ func (a *EnvironmentProgressionAction) Execute(ctx context.Context, trigger acti } version := &actx.Release.Version + getters := &storeGetters{store: a.store} policiesThatCrossedThreshold := a.filterPoliciesWhereThresholdJustCrossed( - ctx, environment, version, actx.Job, progressionDependentPolicies, + ctx, getters, environment, version, actx.Job, progressionDependentPolicies, ) if len(policiesThatCrossedThreshold) == 0 { @@ -89,17 +86,9 @@ func (a *EnvironmentProgressionAction) Execute(ctx context.Context, trigger acti return a.reconcileFn(ctx, progressionDependentTargets) } -func (a *EnvironmentProgressionAction) getEnvironment(envId string) *oapi.Environment { - env, ok := a.getters.GetEnvironment(envId) - if !ok { - return nil - } - return env -} - func (a *EnvironmentProgressionAction) getProgressionDependentPolicies(ctx context.Context, environment *oapi.Environment) ([]*oapi.Policy, error) { policies := make([]*oapi.Policy, 0) - for _, policy := range a.getters.GetPolicies() { + for _, policy := range a.store.Policies.Items() { for _, rule := range policy.Rules { if rule.EnvironmentProgression == nil { continue @@ -126,21 +115,21 @@ func (a *EnvironmentProgressionAction) getProgressionDependentPolicies(ctx conte func (a *EnvironmentProgressionAction) getProgressionDependentTargets(ctx context.Context, policies []*oapi.Policy, deploymentId string) ([]*oapi.ReleaseTarget, error) { targetMap := make(map[string]*oapi.ReleaseTarget) - deploymentTargets, err := a.getters.GetReleaseTargetsForDeployment(ctx, deploymentId) + deploymentTargets, err := a.store.ReleaseTargets.GetForDeployment(ctx, deploymentId) if err != nil { return nil, fmt.Errorf("failed to get deployment targets: %w", err) } for _, target := range deploymentTargets { - environment, ok := a.getters.GetEnvironment(target.EnvironmentId) + environment, ok := a.store.Environments.Get(target.EnvironmentId) if !ok { continue } - resource, ok := a.getters.GetResource(target.ResourceId) + resource, ok := a.store.Resources.Get(target.ResourceId) if !ok { continue } - deployment, ok := a.getters.GetDeployment(target.DeploymentId) + deployment, ok := a.store.Deployments.Get(target.DeploymentId) if !ok { continue } @@ -161,6 +150,7 @@ func (a *EnvironmentProgressionAction) getProgressionDependentTargets(ctx contex func (a *EnvironmentProgressionAction) filterPoliciesWhereThresholdJustCrossed( ctx context.Context, + getters Getters, dependencyEnv *oapi.Environment, version *oapi.DeploymentVersion, job *oapi.Job, @@ -174,7 +164,7 @@ func (a *EnvironmentProgressionAction) filterPoliciesWhereThresholdJustCrossed( continue } - if a.didThresholdJustCross(ctx, dependencyEnv, version, job, rule) { + if a.didThresholdJustCross(ctx, getters, dependencyEnv, version, job, rule) { result = append(result, policy) } } @@ -193,6 +183,7 @@ func (a *EnvironmentProgressionAction) getEnvironmentProgressionRule(policy *oap func (a *EnvironmentProgressionAction) didThresholdJustCross( ctx context.Context, + getters Getters, dependencyEnv *oapi.Environment, version *oapi.DeploymentVersion, job *oapi.Job, @@ -215,7 +206,7 @@ func (a *EnvironmentProgressionAction) didThresholdJustCross( minPercentage = *rule.MinimumSuccessPercentage } - tracker := NewReleaseTargetJobTracker(ctx, a.getters, dependencyEnv, version, successStatuses) + tracker := NewReleaseTargetJobTracker(ctx, getters, dependencyEnv, version, successStatuses) if len(tracker.ReleaseTargets) == 0 { return false diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/getters.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/getters.go index 5391444ab..7800c0ec6 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/getters.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/getters.go @@ -8,15 +8,10 @@ import ( type Getters interface { GetEnvironments() map[string]*oapi.Environment - GetEnvironment(environmentID string) (*oapi.Environment, bool) GetSystemIDsForEnvironment(environmentID string) []string GetReleaseTargetsForEnvironment(ctx context.Context, environmentID string) ([]*oapi.ReleaseTarget, error) - GetReleaseTargetsForDeployment(ctx context.Context, deploymentID string) ([]*oapi.ReleaseTarget, error) GetJobsForReleaseTarget(releaseTarget *oapi.ReleaseTarget) map[string]*oapi.Job GetRelease(releaseID string) (*oapi.Release, bool) - GetResource(resourceID string) (*oapi.Resource, bool) - GetDeployment(deploymentID string) (*oapi.Deployment, bool) - GetPolicies() map[string]*oapi.Policy } var _ Getters = (*storeGetters)(nil) @@ -29,10 +24,6 @@ func (s *storeGetters) GetEnvironments() map[string]*oapi.Environment { return s.store.Environments.Items() } -func (s *storeGetters) GetEnvironment(environmentID string) (*oapi.Environment, bool) { - return s.store.Environments.Get(environmentID) -} - func (s *storeGetters) GetSystemIDsForEnvironment(environmentID string) []string { return s.store.SystemEnvironments.GetSystemIDsForEnvironment(environmentID) } @@ -41,10 +32,6 @@ func (s *storeGetters) GetReleaseTargetsForEnvironment(ctx context.Context, envi return s.store.ReleaseTargets.GetForEnvironment(ctx, environmentID) } -func (s *storeGetters) GetReleaseTargetsForDeployment(ctx context.Context, deploymentID string) ([]*oapi.ReleaseTarget, error) { - return s.store.ReleaseTargets.GetForDeployment(ctx, deploymentID) -} - func (s *storeGetters) GetJobsForReleaseTarget(releaseTarget *oapi.ReleaseTarget) map[string]*oapi.Job { return s.store.Jobs.GetJobsForReleaseTarget(releaseTarget) } @@ -52,15 +39,3 @@ func (s *storeGetters) GetJobsForReleaseTarget(releaseTarget *oapi.ReleaseTarget func (s *storeGetters) GetRelease(releaseID string) (*oapi.Release, bool) { return s.store.Releases.Get(releaseID) } - -func (s *storeGetters) GetResource(resourceID string) (*oapi.Resource, bool) { - return s.store.Resources.Get(resourceID) -} - -func (s *storeGetters) GetDeployment(deploymentID string) (*oapi.Deployment, bool) { - return s.store.Deployments.Get(deploymentID) -} - -func (s *storeGetters) GetPolicies() map[string]*oapi.Policy { - return s.store.Policies.Items() -} diff --git a/apps/workspace-engine/pkg/workspace/workspace.go b/apps/workspace-engine/pkg/workspace/workspace.go index 2e739500b..03d06a409 100644 --- a/apps/workspace-engine/pkg/workspace/workspace.go +++ b/apps/workspace-engine/pkg/workspace/workspace.go @@ -56,7 +56,7 @@ func New(ctx context.Context, id string, options ...WorkspaceOption) *Workspace NewOrchestrator(s). RegisterAction(verificationaction.NewVerificationAction(ws.verificationManager)). RegisterAction(deploymentdependency.NewDeploymentDependencyAction(s, reconcileFn)). - RegisterAction(environmentprogression.NewEnvironmentProgressionActionFromStore(s, reconcileFn)). + RegisterAction(environmentprogression.NewEnvironmentProgressionAction(s, reconcileFn)). RegisterAction(rollback.NewRollbackAction(s, ws.jobAgentRegistry)) ws.workflowActionOrchestrator = workflowmanager. diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/adapters.go b/apps/workspace-engine/svc/controllers/desiredrelease/adapters.go index a3ab27cb2..a426bb5ea 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/adapters.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/adapters.go @@ -1,61 +1,11 @@ package desiredrelease import ( - "context" "time" "workspace-engine/pkg/oapi" - "workspace-engine/pkg/workspace/relationships/eval" - - "github.com/google/uuid" ) -// policyevalAdapter bridges the desiredrelease Getter (which uses -// *ReleaseTarget) to the policyeval.Getter interface (which uses -// *oapi.ReleaseTarget). -type policyevalAdapter struct { - getter Getter - rt *ReleaseTarget -} - -func (a *policyevalAdapter) GetApprovalRecords(ctx context.Context, versionID, environmentID string) ([]*oapi.UserApprovalRecord, error) { - return a.getter.GetApprovalRecords(ctx, versionID, environmentID) -} - -func (a *policyevalAdapter) HasCurrentRelease(ctx context.Context, _ *oapi.ReleaseTarget) (bool, error) { - return a.getter.HasCurrentRelease(ctx, a.rt) -} - -func (a *policyevalAdapter) GetCurrentRelease(ctx context.Context, _ *oapi.ReleaseTarget) (*oapi.Release, error) { - return a.getter.GetCurrentRelease(ctx, a.rt) -} - -func (a *policyevalAdapter) GetPolicySkips(ctx context.Context, versionID, environmentID, resourceID string) ([]*oapi.PolicySkip, error) { - return a.getter.GetPolicySkips(ctx, versionID, environmentID, resourceID) -} - -// variableResolverAdapter bridges the desiredrelease Getter to the -// variableresolver.Getter interface. -type variableResolverAdapter struct { - getter Getter -} - -func (a *variableResolverAdapter) GetDeploymentVariables(ctx context.Context, deploymentID string) ([]oapi.DeploymentVariableWithValues, error) { - return a.getter.GetDeploymentVariables(ctx, deploymentID) -} - -func (a *variableResolverAdapter) GetResourceVariables(ctx context.Context, resourceID string) (map[string]oapi.ResourceVariable, error) { - return a.getter.GetResourceVariables(ctx, resourceID) -} - -func (a *variableResolverAdapter) GetRelationshipRules(ctx context.Context, workspaceID uuid.UUID) ([]eval.Rule, error) { - return a.getter.GetRelationshipRules(ctx, workspaceID) -} - -func (a *variableResolverAdapter) LoadCandidates(ctx context.Context, workspaceID uuid.UUID, entityType string) ([]eval.EntityData, error) { - return a.getter.LoadCandidates(ctx, workspaceID, entityType) -} - func buildRelease(rt *ReleaseTarget, version *oapi.DeploymentVersion, variables map[string]oapi.LiteralValue) *oapi.Release { return &oapi.Release{ ReleaseTarget: oapi.ReleaseTarget{ diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/getters.go b/apps/workspace-engine/svc/controllers/desiredrelease/getters.go index d9cf0ae73..69601ce39 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/getters.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/getters.go @@ -4,28 +4,22 @@ import ( "context" "workspace-engine/pkg/oapi" - "workspace-engine/pkg/workspace/relationships/eval" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator" + "workspace-engine/svc/controllers/desiredrelease/policyeval" + "workspace-engine/svc/controllers/desiredrelease/variableresolver" "github.com/google/uuid" ) type Getter interface { + policyeval.Getter + variableresolver.Getter + ReleaseTargetExists(ctx context.Context, rt *ReleaseTarget) (bool, error) GetReleaseTargetScope(ctx context.Context, rt *ReleaseTarget) (*evaluator.EvaluatorScope, error) GetCandidateVersions(ctx context.Context, deploymentID uuid.UUID) ([]*oapi.DeploymentVersion, error) - GetPolicies(ctx context.Context, rt *ReleaseTarget) ([]*oapi.Policy, error) + + GetPoliciesForReleaseTarget(ctx context.Context, rt *ReleaseTarget) ([]*oapi.Policy, error) - GetApprovalRecords(ctx context.Context, versionID, environmentID string) ([]*oapi.UserApprovalRecord, error) - HasCurrentRelease(ctx context.Context, rt *ReleaseTarget) (bool, error) GetCurrentRelease(ctx context.Context, rt *ReleaseTarget) (*oapi.Release, error) - GetPolicySkips(ctx context.Context, versionID, environmentID, resourceID string) ([]*oapi.PolicySkip, error) - - // Variable resolution - GetDeploymentVariables(ctx context.Context, deploymentID string) ([]oapi.DeploymentVariableWithValues, error) - GetResourceVariables(ctx context.Context, resourceID string) (map[string]oapi.ResourceVariable, error) - - // Realtime relationship resolution - GetRelationshipRules(ctx context.Context, workspaceID uuid.UUID) ([]eval.Rule, error) - LoadCandidates(ctx context.Context, workspaceID uuid.UUID, entityType string) ([]eval.EntityData, error) } diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/getters_postgres.go b/apps/workspace-engine/svc/controllers/desiredrelease/getters_postgres.go index 8864dce09..14ca190eb 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/getters_postgres.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/getters_postgres.go @@ -15,6 +15,8 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +var _ Getter = &PostgresGetter{} + type PostgresGetter struct{} func (g *PostgresGetter) ReleaseTargetExists(ctx context.Context, rt *ReleaseTarget) (bool, error) { @@ -66,7 +68,7 @@ func (g *PostgresGetter) GetCandidateVersions(ctx context.Context, deploymentID return versions, nil } -func (g *PostgresGetter) GetPolicies(ctx context.Context, rt *ReleaseTarget) ([]*oapi.Policy, error) { +func (g *PostgresGetter) GetPoliciesForReleaseTarget(ctx context.Context, rt *ReleaseTarget) ([]*oapi.Policy, error) { policies, err := db.GetQueries(ctx).ListPoliciesByWorkspaceID(ctx, db.ListPoliciesByWorkspaceIDParams{ WorkspaceID: rt.WorkspaceID, Limit: pgtype.Int4{Int32: 5000, Valid: true}, diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/adapters.go b/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/adapters.go deleted file mode 100644 index cead5e065..000000000 --- a/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/adapters.go +++ /dev/null @@ -1,50 +0,0 @@ -package policyeval - -import ( - "context" - - "workspace-engine/pkg/oapi" - "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/approval" - "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deployableversions" - "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentwindow" -) - -var _ approval.Getters = (*approvalAdapter)(nil) - -type approvalAdapter struct { - getter Getter - ctx context.Context -} - -func (a *approvalAdapter) GetApprovalRecords(versionID, environmentID string) []*oapi.UserApprovalRecord { - records, _ := a.getter.GetApprovalRecords(a.ctx, versionID, environmentID) - return records -} - -var _ deploymentwindow.Getters = (*deploymentWindowAdapter)(nil) - -type deploymentWindowAdapter struct { - getter Getter - ctx context.Context -} - -func (a *deploymentWindowAdapter) HasCurrentRelease(ctx context.Context, releaseTarget *oapi.ReleaseTarget) bool { - has, _ := a.getter.HasCurrentRelease(ctx, releaseTarget) - return has -} - -var _ deployableversions.Getters = (*deployableVersionsAdapter)(nil) - -type deployableVersionsAdapter struct { - getter Getter - ctx context.Context - rt *oapi.ReleaseTarget -} - -func (a *deployableVersionsAdapter) GetReleases() map[string]*oapi.Release { - release, _ := a.getter.GetCurrentRelease(a.ctx, a.rt) - if release == nil { - return nil - } - return map[string]*oapi.Release{release.ContentHash(): release} -} diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval.go b/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval.go index c43dc35a4..1e61c9428 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval.go @@ -48,7 +48,7 @@ func ruleEvaluators(ctx context.Context, getter Getter, rule *oapi.PolicyRule) [ func CollectEvaluators(ctx context.Context, getter Getter, rt *oapi.ReleaseTarget, policies []*oapi.Policy) []evaluator.Evaluator { evals := []evaluator.Evaluator{ deployableversions.NewEvaluator( - &deployableVersionsAdapter{getter: getter, ctx: ctx, rt: rt}, + getter, ), } diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go b/apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go index ff4a9ca08..ebc45456d 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go @@ -50,7 +50,7 @@ func (r *reconciler) loadInput(ctx context.Context) error { } r.versions = versions - policies, err := r.getter.GetPolicies(ctx, r.rt) + policies, err := r.getter.GetPoliciesForReleaseTarget(ctx, r.rt) if err != nil { return fmt.Errorf("get policies: %w", err) } @@ -64,10 +64,9 @@ func (r *reconciler) loadInput(ctx context.Context) error { // the earliest NextEvaluationTime when all versions are blocked. func (r *reconciler) findDeployableVersion(ctx context.Context) *time.Time { oapiRT := r.rt.ToOAPI() - evalGetter := &policyevalAdapter{getter: r.getter, rt: r.rt} - evals := policyeval.CollectEvaluators(ctx, evalGetter, oapiRT, r.policies) + evals := policyeval.CollectEvaluators(ctx, r.getter, oapiRT, r.policies) var nextTime *time.Time - r.version, nextTime = policyeval.FindDeployableVersion(ctx, evalGetter, oapiRT, r.versions, evals, *r.scope) + r.version, nextTime = policyeval.FindDeployableVersion(ctx, r.getter, oapiRT, r.versions, evals, *r.scope) return nextTime } @@ -77,9 +76,8 @@ func (r *reconciler) resolveVariables(ctx context.Context) error { Deployment: r.scope.Deployment, Environment: r.scope.Environment, } - varGetter := &variableResolverAdapter{getter: r.getter} vars, err := variableresolver.Resolve( - ctx, varGetter, varScope, + ctx, r.getter, varScope, r.rt.DeploymentID.String(), r.rt.ResourceID.String(), ) if err != nil { diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/related_resolver.go b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/related_resolver.go new file mode 100644 index 000000000..8f32ed673 --- /dev/null +++ b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/related_resolver.go @@ -0,0 +1,89 @@ +package variableresolver + +import ( + "context" + "fmt" + "workspace-engine/pkg/celutil" + "workspace-engine/pkg/oapi" + "workspace-engine/pkg/workspace/relationships/eval" + + "github.com/google/uuid" +) + +// RelatedEntityResolver resolves a reference name to the matched related +// entities for a resource. Implementations may evaluate relationship rules +// in realtime or return pre-computed/mocked results. +type RelatedEntityResolver interface { + ResolveRelated(ctx context.Context, reference string) ([]*eval.EntityData, error) +} + +var _ RelatedEntityResolver = (*realtimeResolver)(nil) + +// realtimeResolver evaluates relationship rules in realtime to resolve +// references, ensuring no stale data is used. +type realtimeResolver struct { + resource *oapi.Resource + workspaceID uuid.UUID + rules []eval.Rule +} + +func newRealtimeResolver(getter Getter, resource *oapi.Resource, workspaceID uuid.UUID, rules []eval.Rule) *realtimeResolver { + return &realtimeResolver{ + resource: resource, + workspaceID: workspaceID, + rules: rules, + } +} + +func (r *realtimeResolver) ResolveRelated(ctx context.Context, reference string) ([]*eval.EntityData, error) { + resID, err := uuid.Parse(r.resource.Id) + if err != nil { + return nil, fmt.Errorf("parse resource id: %w", err) + } + + rawMap, err := celutil.EntityToMap(r.resource) + if err != nil { + return nil, fmt.Errorf("convert resource to map: %w", err) + } + rawMap["type"] = "resource" + + entity := &eval.EntityData{ + ID: resID, + WorkspaceID: r.workspaceID, + EntityType: "resource", + Raw: rawMap, + } + + canadateEntities, err := r.GetAllEntities(ctx, r.workspaceID) + if err != nil { + return nil, fmt.Errorf("get all entities: %w", err) + } + + candidateMap := make(map[string]*eval.EntityData) + for _, entity := range canadateEntities { + candidateMap[entity.EntityType + "-" + entity.ID.String()] = &entity + } + + rules := []eval.Rule{} + for _, rule := range r.rules { + if rule.Reference == reference { + rules = append(rules, rule) + } + } + + matches, err := eval.EvaluateRules(ctx, entity, canadateEntities, rules) + if err != nil { + return nil, err + } + + entities := make([]*eval.EntityData, 0, len(matches)) + for _, match := range matches { + entity, ok := candidateMap[match.ToEntityType + "-" + match.ToEntityID.String()] + if !ok { + return nil, fmt.Errorf("matched entity not found: %s-%s", match.ToEntityType, match.ToEntityID.String()) + } + entities = append(entities, entity) + } + + return entities, nil +} diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go index cec6903aa..aedc7fa8b 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go @@ -5,7 +5,6 @@ import ( "fmt" "sort" - "workspace-engine/pkg/celutil" "workspace-engine/pkg/oapi" "workspace-engine/pkg/selector" "workspace-engine/pkg/workspace/relationships" @@ -25,8 +24,8 @@ var tracer = otel.Tracer("desiredrelease/variableresolver") type Getter interface { GetDeploymentVariables(ctx context.Context, deploymentID string) ([]oapi.DeploymentVariableWithValues, error) GetResourceVariables(ctx context.Context, resourceID string) (map[string]oapi.ResourceVariable, error) - GetRelationshipRules(ctx context.Context, workspaceID uuid.UUID) ([]eval.Rule, error) - LoadCandidates(ctx context.Context, workspaceID uuid.UUID, entityType string) ([]eval.EntityData, error) + GetAllRelationshipRules(ctx context.Context, workspaceID uuid.UUID) ([]eval.Rule, error) + GetAllEntities(ctx context.Context, workspaceID uuid.UUID) ([]eval.EntityData, error) } // Scope carries the already-resolved entities for the release target so the @@ -84,125 +83,47 @@ func Resolve( return nil, fmt.Errorf("parse workspace id: %w", err) } - rules, err := getter.GetRelationshipRules(ctx, wsID) + rules, err := getter.GetAllRelationshipRules(ctx, wsID) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, "get relationship rules failed") return nil, fmt.Errorf("get relationship rules: %w", err) } - resolver := newRealtimeResolver(getter, scope.Resource, wsID, rules) + applicableRules := []eval.Rule{} + for _, rule := range rules { + if rule.Reference == "resource" { + applicableRules = append(applicableRules, rule) + } + } + + resolver := newRealtimeResolver(getter, scope.Resource, wsID, applicableRules) entity := relationships.NewResourceEntity(scope.Resource) resolved := make(map[string]oapi.LiteralValue, len(deploymentVars)) - var fromResource, fromValue, fromDefault int for _, dv := range deploymentVars { key := dv.Variable.Key if lv := resolveFromResource(ctx, resolver, resourceID, key, resourceVars, entity); lv != nil { resolved[key] = *lv - fromResource++ continue } if lv := resolveFromValues(ctx, resolver, resourceID, dv.Values, scope.Resource, entity); lv != nil { resolved[key] = *lv - fromValue++ continue } if dv.Variable.DefaultValue != nil { resolved[key] = *dv.Variable.DefaultValue - fromDefault++ } } - span.SetAttributes( - attribute.Int("resolved.total", len(resolved)), - attribute.Int("resolved.from_resource", fromResource), - attribute.Int("resolved.from_value", fromValue), - attribute.Int("resolved.from_default", fromDefault), - ) return resolved, nil } -// realtimeResolver evaluates relationship rules in realtime to resolve -// references, ensuring no stale data is used. -type realtimeResolver struct { - getter Getter - resource *oapi.Resource - workspaceID uuid.UUID - rules []eval.Rule -} - -func newRealtimeResolver(getter Getter, resource *oapi.Resource, workspaceID uuid.UUID, rules []eval.Rule) *realtimeResolver { - return &realtimeResolver{ - getter: getter, - resource: resource, - workspaceID: workspaceID, - rules: rules, - } -} - -func (r *realtimeResolver) LoadCandidates(ctx context.Context, workspaceID uuid.UUID, entityType string) ([]eval.EntityData, error) { - return r.getter.LoadCandidates(ctx, workspaceID, entityType) -} - -func (r *realtimeResolver) ResolveRelated(ctx context.Context, reference string) ([]*oapi.RelatableEntity, error) { - resID, err := uuid.Parse(r.resource.Id) - if err != nil { - return nil, fmt.Errorf("parse resource id: %w", err) - } - - rawMap, err := celutil.EntityToMap(r.resource) - if err != nil { - return nil, fmt.Errorf("convert resource to map: %w", err) - } - rawMap["type"] = "resource" - - entity := &eval.EntityData{ - ID: resID, - WorkspaceID: r.workspaceID, - EntityType: "resource", - Raw: rawMap, - } - - matches, err := eval.ResolveForReference(ctx, r, entity, r.rules, reference) - if err != nil { - return nil, err - } - - var result []*oapi.RelatableEntity - for _, m := range matches { - relatedID := m.ToEntityID - relatedType := m.ToEntityType - if m.ToEntityID == resID { - relatedID = m.FromEntityID - relatedType = m.FromEntityType - } - - candidates, err := r.getter.LoadCandidates(ctx, r.workspaceID, relatedType) - if err != nil { - return nil, fmt.Errorf("load candidate for matched entity %s: %w", relatedID, err) - } - - for i := range candidates { - if candidates[i].ID == relatedID { - re, err := entityDataToRelatableEntity(&candidates[i]) - if err != nil { - return nil, fmt.Errorf("convert matched entity: %w", err) - } - result = append(result, re) - break - } - } - } - - return result, nil -} - // resolveFromResource checks if a resource variable exists for the given key // and resolves it. func resolveFromResource( diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/utils.go b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/utils.go new file mode 100644 index 000000000..d075e3209 --- /dev/null +++ b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/utils.go @@ -0,0 +1,101 @@ +package variableresolver + +import ( + "fmt" + "reflect" + "strings" + "workspace-engine/pkg/oapi" +) + +// getPropertyReflection uses reflection to get a property value (fallback method) +func getPropertyReflection(entity any, propertyPath []string) (*oapi.LiteralValue, error) { + if len(propertyPath) == 0 { + return convertValue(entity) + } + + v := reflect.ValueOf(entity) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + if v.Kind() != reflect.Struct { + return nil, fmt.Errorf("entity is not a struct") + } + + // Get the first property + fieldName := propertyPath[0] + field := v.FieldByName(fieldName) + if !field.IsValid() { + // Try case-insensitive match + field = v.FieldByNameFunc(func(name string) bool { + return strings.EqualFold(name, fieldName) + }) + if !field.IsValid() { + return nil, fmt.Errorf("field %s not found", fieldName) + } + } + + // If this is the last element, return the field value + if len(propertyPath) == 1 { + return convertValue(field.Interface()) + } + + return getPropertyReflection(field.Interface(), propertyPath[1:]) +} + + +// convertValue converts a Go value to an oapi.LiteralValue +func convertValue(val any) (*oapi.LiteralValue, error) { + switch v := val.(type) { + case *oapi.LiteralValue: + return v, nil + case string: + lv := &oapi.LiteralValue{} + err := lv.FromStringValue(v) + return lv, err + case float64: + lv := &oapi.LiteralValue{} + err := lv.FromNumberValue(float32(v)) + return lv, err + case float32: + lv := &oapi.LiteralValue{} + err := lv.FromNumberValue(v) + return lv, err + case int: + lv := &oapi.LiteralValue{} + err := lv.FromIntegerValue(v) + return lv, err + case int32: + lv := &oapi.LiteralValue{} + err := lv.FromIntegerValue(int(v)) + return lv, err + case int64: + lv := &oapi.LiteralValue{} + err := lv.FromIntegerValue(int(v)) + return lv, err + case bool: + lv := &oapi.LiteralValue{} + err := lv.FromBooleanValue(v) + return lv, err + case map[string]any: + lv := &oapi.LiteralValue{} + err := lv.FromObjectValue(oapi.ObjectValue{Object: v}) + return lv, err + case map[string]string: + // Convert map[string]string to map[string]any + m := make(map[string]any, len(v)) + for k, val := range v { + m[k] = val + } + lv := &oapi.LiteralValue{} + err := lv.FromObjectValue(oapi.ObjectValue{Object: m}) + return lv, err + case nil: + lv := &oapi.LiteralValue{} + err := lv.FromNullValue(true) + return lv, err + default: + return nil, fmt.Errorf("unexpected variable value type: %T", val) + } +} + diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/value.go b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/value.go index 75e7b2fd7..055891c24 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/value.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/value.go @@ -5,16 +5,8 @@ import ( "fmt" "workspace-engine/pkg/oapi" - "workspace-engine/pkg/workspace/relationships" ) -// RelatedEntityResolver resolves a reference name to the matched related -// entities for a resource. Implementations may evaluate relationship rules -// in realtime or return pre-computed/mocked results. -type RelatedEntityResolver interface { - ResolveRelated(ctx context.Context, reference string) ([]*oapi.RelatableEntity, error) -} - // ResolveValue resolves a single oapi.Value to a concrete LiteralValue. // // Literal values are returned as-is. Reference values are resolved by @@ -78,7 +70,7 @@ func resolveReference( ) } - lv, err := relationships.GetPropertyValue(refs[0], rv.Path) + lv, err := getPropertyReflection(refs[0].Raw, rv.Path) if err != nil { return nil, fmt.Errorf("resolve property path %v on reference %q: %w", rv.Path, rv.Reference, err) } From f4ea0f88566dcd9cf777046b95b430bba8fc060c Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 6 Mar 2026 03:26:28 -0500 Subject: [PATCH 04/10] refactor: remove RelatedEntityResolver and streamline variable resolution process by integrating direct entity access --- .../controllers/desiredrelease/reconcile.go | 1 - .../variableresolver/related_resolver.go | 89 ------ .../variableresolver/resolve.go | 198 ++++-------- .../variableresolver/resolve_context.go | 47 +++ .../variableresolver/resolve_test.go | 283 ++++++------------ .../desiredrelease/variableresolver/utils.go | 60 ++-- .../desiredrelease/variableresolver/value.go | 80 ++--- 7 files changed, 260 insertions(+), 498 deletions(-) delete mode 100644 apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/related_resolver.go create mode 100644 apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_context.go diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go b/apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go index ebc45456d..272481858 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go @@ -78,7 +78,6 @@ func (r *reconciler) resolveVariables(ctx context.Context) error { } vars, err := variableresolver.Resolve( ctx, r.getter, varScope, - r.rt.DeploymentID.String(), r.rt.ResourceID.String(), ) if err != nil { return err diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/related_resolver.go b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/related_resolver.go deleted file mode 100644 index 8f32ed673..000000000 --- a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/related_resolver.go +++ /dev/null @@ -1,89 +0,0 @@ -package variableresolver - -import ( - "context" - "fmt" - "workspace-engine/pkg/celutil" - "workspace-engine/pkg/oapi" - "workspace-engine/pkg/workspace/relationships/eval" - - "github.com/google/uuid" -) - -// RelatedEntityResolver resolves a reference name to the matched related -// entities for a resource. Implementations may evaluate relationship rules -// in realtime or return pre-computed/mocked results. -type RelatedEntityResolver interface { - ResolveRelated(ctx context.Context, reference string) ([]*eval.EntityData, error) -} - -var _ RelatedEntityResolver = (*realtimeResolver)(nil) - -// realtimeResolver evaluates relationship rules in realtime to resolve -// references, ensuring no stale data is used. -type realtimeResolver struct { - resource *oapi.Resource - workspaceID uuid.UUID - rules []eval.Rule -} - -func newRealtimeResolver(getter Getter, resource *oapi.Resource, workspaceID uuid.UUID, rules []eval.Rule) *realtimeResolver { - return &realtimeResolver{ - resource: resource, - workspaceID: workspaceID, - rules: rules, - } -} - -func (r *realtimeResolver) ResolveRelated(ctx context.Context, reference string) ([]*eval.EntityData, error) { - resID, err := uuid.Parse(r.resource.Id) - if err != nil { - return nil, fmt.Errorf("parse resource id: %w", err) - } - - rawMap, err := celutil.EntityToMap(r.resource) - if err != nil { - return nil, fmt.Errorf("convert resource to map: %w", err) - } - rawMap["type"] = "resource" - - entity := &eval.EntityData{ - ID: resID, - WorkspaceID: r.workspaceID, - EntityType: "resource", - Raw: rawMap, - } - - canadateEntities, err := r.GetAllEntities(ctx, r.workspaceID) - if err != nil { - return nil, fmt.Errorf("get all entities: %w", err) - } - - candidateMap := make(map[string]*eval.EntityData) - for _, entity := range canadateEntities { - candidateMap[entity.EntityType + "-" + entity.ID.String()] = &entity - } - - rules := []eval.Rule{} - for _, rule := range r.rules { - if rule.Reference == reference { - rules = append(rules, rule) - } - } - - matches, err := eval.EvaluateRules(ctx, entity, canadateEntities, rules) - if err != nil { - return nil, err - } - - entities := make([]*eval.EntityData, 0, len(matches)) - for _, match := range matches { - entity, ok := candidateMap[match.ToEntityType + "-" + match.ToEntityID.String()] - if !ok { - return nil, fmt.Errorf("matched entity not found: %s-%s", match.ToEntityType, match.ToEntityID.String()) - } - entities = append(entities, entity) - } - - return entities, nil -} diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go index aedc7fa8b..8d7915575 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go @@ -5,9 +5,9 @@ import ( "fmt" "sort" + "workspace-engine/pkg/celutil" "workspace-engine/pkg/oapi" "workspace-engine/pkg/selector" - "workspace-engine/pkg/workspace/relationships" "workspace-engine/pkg/workspace/relationships/eval" "github.com/google/uuid" @@ -19,18 +19,15 @@ import ( var tracer = otel.Tracer("desiredrelease/variableresolver") // Getter provides the data needed to resolve deployment variables for a -// release target. Implementations backed by Postgres or in-memory mocks -// both satisfy this interface. +// release target. type Getter interface { GetDeploymentVariables(ctx context.Context, deploymentID string) ([]oapi.DeploymentVariableWithValues, error) GetResourceVariables(ctx context.Context, resourceID string) (map[string]oapi.ResourceVariable, error) - GetAllRelationshipRules(ctx context.Context, workspaceID uuid.UUID) ([]eval.Rule, error) + GetRelationshipRules(ctx context.Context, workspaceID uuid.UUID) ([]eval.Rule, error) GetAllEntities(ctx context.Context, workspaceID uuid.UUID) ([]eval.EntityData, error) } -// Scope carries the already-resolved entities for the release target so the -// resolver can evaluate CEL resource selectors and resolve reference -// variables without additional lookups. +// Scope carries the already-resolved entities for the release target. type Scope struct { Resource *oapi.Resource Deployment *oapi.Deployment @@ -48,11 +45,13 @@ func Resolve( ctx context.Context, getter Getter, scope *Scope, - deploymentID, resourceID string, ) (map[string]oapi.LiteralValue, error) { ctx, span := tracer.Start(ctx, "variableresolver.Resolve") defer span.End() + deploymentID := scope.Deployment.Id + resourceID := scope.Resource.Id + span.SetAttributes( attribute.String("deployment.id", deploymentID), attribute.String("resource.id", resourceID), @@ -78,40 +77,24 @@ func Resolve( } span.SetAttributes(attribute.Int("resource_variables.count", len(resourceVars))) - wsID, err := uuid.Parse(scope.Resource.WorkspaceId) - if err != nil { - return nil, fmt.Errorf("parse workspace id: %w", err) - } - - rules, err := getter.GetAllRelationshipRules(ctx, wsID) + rc, err := buildResolveContext(ctx, getter, scope.Resource) if err != nil { span.RecordError(err) - span.SetStatus(codes.Error, "get relationship rules failed") - return nil, fmt.Errorf("get relationship rules: %w", err) - } - - applicableRules := []eval.Rule{} - for _, rule := range rules { - if rule.Reference == "resource" { - applicableRules = append(applicableRules, rule) - } + return nil, err } - resolver := newRealtimeResolver(getter, scope.Resource, wsID, applicableRules) - - entity := relationships.NewResourceEntity(scope.Resource) - resolved := make(map[string]oapi.LiteralValue, len(deploymentVars)) - for _, dv := range deploymentVars { key := dv.Variable.Key - if lv := resolveFromResource(ctx, resolver, resourceID, key, resourceVars, entity); lv != nil { - resolved[key] = *lv - continue + if rv, ok := resourceVars[key]; ok { + if lv := resolveValue(ctx, rc.resolveRelated, &rv.Value); lv != nil { + resolved[key] = *lv + continue + } } - if lv := resolveFromValues(ctx, resolver, resourceID, dv.Values, scope.Resource, entity); lv != nil { + if lv := resolveFromValues(ctx, rc, dv.Values, scope.Resource); lv != nil { resolved[key] = *lv continue } @@ -124,36 +107,56 @@ func Resolve( return resolved, nil } -// resolveFromResource checks if a resource variable exists for the given key -// and resolves it. -func resolveFromResource( - ctx context.Context, - resolver RelatedEntityResolver, - resourceID string, - key string, - resourceVars map[string]oapi.ResourceVariable, - entity *oapi.RelatableEntity, -) *oapi.LiteralValue { - rv, ok := resourceVars[key] - if !ok { - return nil +func buildResolveContext(ctx context.Context, getter Getter, resource *oapi.Resource) (*resolveContext, error) { + wsID, err := uuid.Parse(resource.WorkspaceId) + if err != nil { + return nil, fmt.Errorf("parse workspace id: %w", err) } - lv, err := ResolveValue(ctx, resolver, resourceID, entity, &rv.Value) + resID, err := uuid.Parse(resource.Id) if err != nil { - return nil + return nil, fmt.Errorf("parse resource id: %w", err) + } + + rules, err := getter.GetRelationshipRules(ctx, wsID) + if err != nil { + return nil, fmt.Errorf("get relationship rules: %w", err) + } + + allCandidates, err := getter.GetAllEntities(ctx, wsID) + if err != nil { + return nil, fmt.Errorf("get all entities: %w", err) + } + + rawMap, err := celutil.EntityToMap(resource) + if err != nil { + return nil, fmt.Errorf("convert resource to CEL map: %w", err) + } + rawMap["type"] = "resource" + + candidateIdx := make(map[string]*eval.EntityData, len(allCandidates)) + for i := range allCandidates { + c := &allCandidates[i] + candidateIdx[c.EntityType+"-"+c.ID.String()] = c } - return lv + + return &resolveContext{ + entity: &eval.EntityData{ + ID: resID, WorkspaceID: wsID, + EntityType: "resource", Raw: rawMap, + }, + rules: rules, + candidates: allCandidates, + candidateIdx: candidateIdx, + }, nil } // resolveFromValues finds the highest-priority deployment variable value // whose resource selector matches the target resource, then resolves it. func resolveFromValues( ctx context.Context, - resolver RelatedEntityResolver, - resourceID string, + rc *resolveContext, values []oapi.DeploymentVariableValue, resource *oapi.Resource, - entity *oapi.RelatableEntity, ) *oapi.LiteralValue { matched := make([]oapi.DeploymentVariableValue, 0, len(values)) for _, v := range values { @@ -175,100 +178,9 @@ func resolveFromValues( }) for _, v := range matched { - lv, err := ResolveValue(ctx, resolver, resourceID, entity, &v.Value) - if err == nil && lv != nil { + if lv := resolveValue(ctx, rc.resolveRelated, &v.Value); lv != nil { return lv } } return nil } - -func entityDataToRelatableEntity(data *eval.EntityData) (*oapi.RelatableEntity, error) { - entity := &oapi.RelatableEntity{} - - switch data.EntityType { - case "resource": - r := mapToResource(data) - if err := entity.FromResource(r); err != nil { - return nil, err - } - case "deployment": - d := mapToDeployment(data) - if err := entity.FromDeployment(d); err != nil { - return nil, err - } - case "environment": - e := mapToEnvironment(data) - if err := entity.FromEnvironment(e); err != nil { - return nil, err - } - default: - return nil, fmt.Errorf("unsupported entity type: %s", data.EntityType) - } - - return entity, nil -} - -func mapToResource(data *eval.EntityData) oapi.Resource { - raw := data.Raw - r := oapi.Resource{ - Id: data.ID.String(), - WorkspaceId: data.WorkspaceID.String(), - } - if v, ok := raw["name"].(string); ok { - r.Name = v - } - if v, ok := raw["kind"].(string); ok { - r.Kind = v - } - if v, ok := raw["version"].(string); ok { - r.Version = v - } - if v, ok := raw["identifier"].(string); ok { - r.Identifier = v - } - if v, ok := raw["config"].(map[string]any); ok { - r.Config = v - } - if v, ok := raw["metadata"].(map[string]any); ok { - md := make(map[string]string, len(v)) - for k, val := range v { - if s, ok := val.(string); ok { - md[k] = s - } - } - r.Metadata = md - } - return r -} - -func mapToDeployment(data *eval.EntityData) oapi.Deployment { - raw := data.Raw - d := oapi.Deployment{ - Id: data.ID.String(), - } - if v, ok := raw["name"].(string); ok { - d.Name = v - } - if v, ok := raw["slug"].(string); ok { - d.Slug = v - } - if v, ok := raw["description"].(string); ok { - d.Description = &v - } - return d -} - -func mapToEnvironment(data *eval.EntityData) oapi.Environment { - raw := data.Raw - e := oapi.Environment{ - Id: data.ID.String(), - } - if v, ok := raw["name"].(string); ok { - e.Name = v - } - if v, ok := raw["description"].(string); ok { - e.Description = &v - } - return e -} diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_context.go b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_context.go new file mode 100644 index 000000000..0cee3f611 --- /dev/null +++ b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_context.go @@ -0,0 +1,47 @@ +package variableresolver + +import ( + "context" + "workspace-engine/pkg/workspace/relationships/eval" +) + +// resolveContext holds all pre-loaded data needed for a single Resolve call. +// This avoids repeated data loading and duplicate rule filtering. +type resolveContext struct { + entity *eval.EntityData + rules []eval.Rule + candidates []eval.EntityData + candidateIdx map[string]*eval.EntityData +} + +// resolveRelated finds the first entity matching a reference name by +// evaluating only the rules that apply to that reference. +func (rc *resolveContext) resolveRelated(ctx context.Context, reference string) (*eval.EntityData, error) { + filtered := make([]eval.Rule, 0, len(rc.rules)) + for _, r := range rc.rules { + if r.Reference == reference { + filtered = append(filtered, r) + } + } + if len(filtered) == 0 { + return nil, nil + } + + matches, err := eval.EvaluateRules(ctx, rc.entity, rc.candidates, filtered) + if err != nil { + return nil, err + } + + for _, m := range matches { + relatedID := m.ToEntityID + relatedType := m.ToEntityType + if m.ToEntityID == rc.entity.ID { + relatedID = m.FromEntityID + relatedType = m.FromEntityType + } + if c, ok := rc.candidateIdx[relatedType+"-"+relatedID.String()]; ok { + return c, nil + } + } + return nil, nil +} \ No newline at end of file diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go index 422d3e97c..043d9faa8 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve_test.go @@ -13,19 +13,7 @@ import ( ) // --------------------------------------------------------------------------- -// mock RelatedEntityResolver (for ResolveValue tests) -// --------------------------------------------------------------------------- - -type mockResolver struct { - related map[string][]*oapi.RelatableEntity -} - -func (m *mockResolver) ResolveRelated(_ context.Context, reference string) ([]*oapi.RelatableEntity, error) { - return m.related[reference], nil -} - -// --------------------------------------------------------------------------- -// mock Getter (for Resolve tests) +// mock Getter // --------------------------------------------------------------------------- type mockGetter struct { @@ -103,39 +91,28 @@ func referenceValue(ref string, path ...string) oapi.Value { return *v } -func makeResourceEntity(res *oapi.Resource) oapi.RelatableEntity { - e := oapi.RelatableEntity{} - _ = e.FromResource(*res) - return e -} - -func makeDeploymentEntity(dep *oapi.Deployment) oapi.RelatableEntity { - e := oapi.RelatableEntity{} - _ = e.FromDeployment(*dep) - return e +// mockLookup builds a relatedLookup from a map of reference → entity data. +func mockLookup(related map[string]*eval.EntityData) relatedLookup { + return func(_ context.Context, reference string) (*eval.EntityData, error) { + return related[reference], nil + } } -func makeEnvironmentEntity(env *oapi.Environment) oapi.RelatableEntity { - e := oapi.RelatableEntity{} - _ = e.FromEnvironment(*env) - return e +// noopLookup is a lookup that always returns nil (for literal-only tests). +var noopLookup relatedLookup = func(_ context.Context, _ string) (*eval.EntityData, error) { + return nil, nil } -// emptyResolver is a no-op resolver used for literal-only tests. -var emptyResolver = &mockResolver{} - // emptyGetter is a no-op getter used for Resolve tests without references. var emptyGetter = &mockGetter{} // --------------------------------------------------------------------------- -// ResolveValue tests — literal +// resolveValue tests — literal // --------------------------------------------------------------------------- func TestResolveValue_Literal_String(t *testing.T) { - scope := newScope() val := literalStringValue("hello") - entity := makeResourceEntity(scope.Resource) - lv, err := ResolveValue(context.Background(), emptyResolver, scope.Resource.Id, &entity, &val) + lv, err := resolveValue(context.Background(), noopLookup, &val) require.NoError(t, err) s, err := lv.AsStringValue() require.NoError(t, err) @@ -143,10 +120,8 @@ func TestResolveValue_Literal_String(t *testing.T) { } func TestResolveValue_Literal_Int(t *testing.T) { - scope := newScope() val := literalIntValue(42) - entity := makeResourceEntity(scope.Resource) - lv, err := ResolveValue(context.Background(), emptyResolver, scope.Resource.Id, &entity, &val) + lv, err := resolveValue(context.Background(), noopLookup, &val) require.NoError(t, err) i, err := lv.AsIntegerValue() require.NoError(t, err) @@ -154,10 +129,8 @@ func TestResolveValue_Literal_Int(t *testing.T) { } func TestResolveValue_Literal_Bool(t *testing.T) { - scope := newScope() val := literalBoolValue(true) - entity := makeResourceEntity(scope.Resource) - lv, err := ResolveValue(context.Background(), emptyResolver, scope.Resource.Id, &entity, &val) + lv, err := resolveValue(context.Background(), noopLookup, &val) require.NoError(t, err) b, err := lv.AsBooleanValue() require.NoError(t, err) @@ -165,31 +138,19 @@ func TestResolveValue_Literal_Bool(t *testing.T) { } // --------------------------------------------------------------------------- -// ResolveValue tests — reference +// resolveValue tests — reference // --------------------------------------------------------------------------- func TestResolveValue_Reference_ResourceName(t *testing.T) { - scope := newScope() - entity := makeResourceEntity(scope.Resource) - relatedResource := &oapi.Resource{ - Id: uuid.New().String(), - Name: "db-server", - Kind: "Database", - Version: "v1", - Identifier: "db-server", - WorkspaceId: scope.Resource.WorkspaceId, - Metadata: map[string]string{}, - Config: map[string]any{}, - } - relatedEntity := makeResourceEntity(relatedResource) - resolver := &mockResolver{ - related: map[string][]*oapi.RelatableEntity{ - "database": {&relatedEntity}, + lookup := mockLookup(map[string]*eval.EntityData{ + "database": { + ID: uuid.New(), EntityType: "resource", + Raw: map[string]any{"name": "db-server", "kind": "Database"}, }, - } + }) val := referenceValue("database", "name") - lv, err := ResolveValue(context.Background(), resolver, scope.Resource.Id, &entity, &val) + lv, err := resolveValue(context.Background(), lookup, &val) require.NoError(t, err) s, err := lv.AsStringValue() require.NoError(t, err) @@ -197,27 +158,18 @@ func TestResolveValue_Reference_ResourceName(t *testing.T) { } func TestResolveValue_Reference_ResourceMetadata(t *testing.T) { - scope := newScope() - entity := makeResourceEntity(scope.Resource) - relatedResource := &oapi.Resource{ - Id: uuid.New().String(), - Name: "vpc", - Kind: "Network", - Version: "v1", - Identifier: "vpc", - WorkspaceId: scope.Resource.WorkspaceId, - Metadata: map[string]string{"cidr": "10.0.0.0/16"}, - Config: map[string]any{}, - } - relatedEntity := makeResourceEntity(relatedResource) - resolver := &mockResolver{ - related: map[string][]*oapi.RelatableEntity{ - "network": {&relatedEntity}, + lookup := mockLookup(map[string]*eval.EntityData{ + "network": { + ID: uuid.New(), EntityType: "resource", + Raw: map[string]any{ + "name": "vpc", + "metadata": map[string]any{"cidr": "10.0.0.0/16"}, + }, }, - } + }) val := referenceValue("network", "metadata", "cidr") - lv, err := ResolveValue(context.Background(), resolver, scope.Resource.Id, &entity, &val) + lv, err := resolveValue(context.Background(), lookup, &val) require.NoError(t, err) s, err := lv.AsStringValue() require.NoError(t, err) @@ -225,22 +177,15 @@ func TestResolveValue_Reference_ResourceMetadata(t *testing.T) { } func TestResolveValue_Reference_DeploymentName(t *testing.T) { - scope := newScope() - entity := makeResourceEntity(scope.Resource) - relatedDep := &oapi.Deployment{ - Id: uuid.New().String(), - Name: "api-service", - Slug: "api-service", - } - relatedEntity := makeDeploymentEntity(relatedDep) - resolver := &mockResolver{ - related: map[string][]*oapi.RelatableEntity{ - "parent-deployment": {&relatedEntity}, + lookup := mockLookup(map[string]*eval.EntityData{ + "parent-deployment": { + ID: uuid.New(), EntityType: "deployment", + Raw: map[string]any{"name": "api-service", "slug": "api-service"}, }, - } + }) val := referenceValue("parent-deployment", "name") - lv, err := ResolveValue(context.Background(), resolver, scope.Resource.Id, &entity, &val) + lv, err := resolveValue(context.Background(), lookup, &val) require.NoError(t, err) s, err := lv.AsStringValue() require.NoError(t, err) @@ -248,21 +193,15 @@ func TestResolveValue_Reference_DeploymentName(t *testing.T) { } func TestResolveValue_Reference_EnvironmentName(t *testing.T) { - scope := newScope() - entity := makeResourceEntity(scope.Resource) - relatedEnv := &oapi.Environment{ - Id: uuid.New().String(), - Name: "staging", - } - relatedEntity := makeEnvironmentEntity(relatedEnv) - resolver := &mockResolver{ - related: map[string][]*oapi.RelatableEntity{ - "env": {&relatedEntity}, + lookup := mockLookup(map[string]*eval.EntityData{ + "env": { + ID: uuid.New(), EntityType: "environment", + Raw: map[string]any{"name": "staging"}, }, - } + }) val := referenceValue("env", "name") - lv, err := ResolveValue(context.Background(), resolver, scope.Resource.Id, &entity, &val) + lv, err := resolveValue(context.Background(), lookup, &val) require.NoError(t, err) s, err := lv.AsStringValue() require.NoError(t, err) @@ -270,40 +209,58 @@ func TestResolveValue_Reference_EnvironmentName(t *testing.T) { } func TestResolveValue_Reference_NotFound(t *testing.T) { - scope := newScope() - entity := makeResourceEntity(scope.Resource) val := referenceValue("nonexistent", "name") - _, err := ResolveValue(context.Background(), emptyResolver, scope.Resource.Id, &entity, &val) - require.Error(t, err) - assert.Contains(t, err.Error(), "not found") + lv, err := resolveValue(context.Background(), noopLookup, &val) + require.NoError(t, err) + assert.Nil(t, lv, "unresolved reference should return nil so callers fall through") } func TestResolveValue_Reference_BadPath(t *testing.T) { - scope := newScope() - entity := makeResourceEntity(scope.Resource) - relatedResource := &oapi.Resource{ - Id: uuid.New().String(), - Name: "db", - Kind: "Database", - Version: "v1", - Identifier: "db", - WorkspaceId: scope.Resource.WorkspaceId, - Metadata: map[string]string{}, - Config: map[string]any{}, - } - relatedEntity := makeResourceEntity(relatedResource) - resolver := &mockResolver{ - related: map[string][]*oapi.RelatableEntity{ - "database": {&relatedEntity}, + lookup := mockLookup(map[string]*eval.EntityData{ + "database": { + ID: uuid.New(), EntityType: "resource", + Raw: map[string]any{ + "name": "db", + "metadata": map[string]any{}, + }, }, - } + }) val := referenceValue("database", "metadata", "missing_key") - _, err := ResolveValue(context.Background(), resolver, scope.Resource.Id, &entity, &val) + _, err := resolveValue(context.Background(), lookup, &val) require.Error(t, err) assert.Contains(t, err.Error(), "not found") } +func TestResolveValue_Reference_NestedConfig(t *testing.T) { + lookup := mockLookup(map[string]*eval.EntityData{ + "self": { + ID: uuid.New(), EntityType: "resource", + Raw: map[string]any{ + "config": map[string]any{ + "networking": map[string]any{"vpc_id": "vpc-12345"}, + }, + }, + }, + }) + + val := referenceValue("self", "config", "networking", "vpc_id") + lv, err := resolveValue(context.Background(), lookup, &val) + require.NoError(t, err) + s, err := lv.AsStringValue() + require.NoError(t, err) + assert.Equal(t, "vpc-12345", string(s)) +} + +func TestResolveValue_Sensitive_ReturnsError(t *testing.T) { + v := &oapi.Value{} + _ = v.FromSensitiveValue(oapi.SensitiveValue{ValueHash: "abc123"}) + + _, err := resolveValue(context.Background(), noopLookup, v) + require.Error(t, err) + assert.Contains(t, err.Error(), "sensitive") +} + // --------------------------------------------------------------------------- // Resolve tests — priority: resource var wins // --------------------------------------------------------------------------- @@ -336,7 +293,7 @@ func TestResolve_ResourceVarWins(t *testing.T) { }, } - resolved, err := Resolve(context.Background(), getter, scope, scope.Deployment.Id, scope.Resource.Id) + resolved, err := Resolve(context.Background(), getter, scope) require.NoError(t, err) require.Contains(t, resolved, "region") s, err := resolved["region"].AsStringValue() @@ -370,7 +327,7 @@ func TestResolve_DeploymentVariableValueUsedWhenNoResourceVar(t *testing.T) { resourceVars: map[string]oapi.ResourceVariable{}, } - resolved, err := Resolve(context.Background(), getter, scope, scope.Deployment.Id, scope.Resource.Id) + resolved, err := Resolve(context.Background(), getter, scope) require.NoError(t, err) require.Contains(t, resolved, "image") s, err := resolved["image"].AsStringValue() @@ -398,7 +355,7 @@ func TestResolve_DefaultValueFallback(t *testing.T) { resourceVars: map[string]oapi.ResourceVariable{}, } - resolved, err := Resolve(context.Background(), getter, scope, scope.Deployment.Id, scope.Resource.Id) + resolved, err := Resolve(context.Background(), getter, scope) require.NoError(t, err) require.Contains(t, resolved, "replicas") i, err := resolved["replicas"].AsIntegerValue() @@ -425,7 +382,7 @@ func TestResolve_NoMatchNoDefault_KeyAbsent(t *testing.T) { resourceVars: map[string]oapi.ResourceVariable{}, } - resolved, err := Resolve(context.Background(), getter, scope, scope.Deployment.Id, scope.Resource.Id) + resolved, err := Resolve(context.Background(), getter, scope) require.NoError(t, err) assert.NotContains(t, resolved, "optional") } @@ -469,7 +426,7 @@ func TestResolve_HighestPriorityValueWins(t *testing.T) { resourceVars: map[string]oapi.ResourceVariable{}, } - resolved, err := Resolve(context.Background(), getter, scope, scope.Deployment.Id, scope.Resource.Id) + resolved, err := Resolve(context.Background(), getter, scope) require.NoError(t, err) s, err := resolved["image"].AsStringValue() require.NoError(t, err) @@ -516,7 +473,7 @@ func TestResolve_MultipleVariables(t *testing.T) { resourceVars: map[string]oapi.ResourceVariable{}, } - resolved, err := Resolve(context.Background(), getter, scope, scope.Deployment.Id, scope.Resource.Id) + resolved, err := Resolve(context.Background(), getter, scope) require.NoError(t, err) assert.Len(t, resolved, 3) @@ -537,7 +494,7 @@ func TestResolve_MultipleVariables(t *testing.T) { func TestResolve_NoDeploymentVars_EmptyMap(t *testing.T) { scope := newScope() - resolved, err := Resolve(context.Background(), emptyGetter, scope, scope.Deployment.Id, scope.Resource.Id) + resolved, err := Resolve(context.Background(), emptyGetter, scope) require.NoError(t, err) assert.Empty(t, resolved) } @@ -550,16 +507,6 @@ func TestResolve_ResourceVar_WithReference(t *testing.T) { scope := newScope() resourceID := uuid.MustParse(scope.Resource.Id) relatedResourceID := uuid.New() - relatedResource := &oapi.Resource{ - Id: relatedResourceID.String(), - Name: "db-server", - Kind: "Database", - Version: "v1", - Identifier: "db-server", - WorkspaceId: scope.Resource.WorkspaceId, - Metadata: map[string]string{"host": "db.internal"}, - Config: map[string]any{}, - } ruleID := uuid.New() getter := &mockGetter{ @@ -601,7 +548,7 @@ func TestResolve_ResourceVar_WithReference(t *testing.T) { EntityType: "resource", Raw: map[string]any{ "type": "resource", "id": relatedResourceID.String(), - "name": relatedResource.Name, "kind": relatedResource.Kind, + "name": "db-server", "kind": "Database", "version": "v1", "identifier": "db-server", "config": map[string]any{}, "metadata": map[string]any{"host": "db.internal"}, }, @@ -610,7 +557,7 @@ func TestResolve_ResourceVar_WithReference(t *testing.T) { }, } - resolved, err := Resolve(context.Background(), getter, scope, scope.Deployment.Id, scope.Resource.Id) + resolved, err := Resolve(context.Background(), getter, scope) require.NoError(t, err) require.Contains(t, resolved, "db_host") s, err := resolved["db_host"].AsStringValue() @@ -677,7 +624,7 @@ func TestResolve_DeploymentVarValue_WithReference(t *testing.T) { }, } - resolved, err := Resolve(context.Background(), getter, scope, scope.Deployment.Id, scope.Resource.Id) + resolved, err := Resolve(context.Background(), getter, scope) require.NoError(t, err) require.Contains(t, resolved, "cluster_endpoint") s, err := resolved["cluster_endpoint"].AsStringValue() @@ -760,7 +707,7 @@ func TestResolve_MixedLiteralAndReference(t *testing.T) { }, } - resolved, err := Resolve(context.Background(), getter, scope, scope.Deployment.Id, scope.Resource.Id) + resolved, err := Resolve(context.Background(), getter, scope) require.NoError(t, err) assert.Len(t, resolved, 2) @@ -803,52 +750,10 @@ func TestResolve_ResourceVarRefFails_FallsToDeploymentValue(t *testing.T) { rules: []eval.Rule{}, } - resolved, err := Resolve(context.Background(), getter, scope, scope.Deployment.Id, scope.Resource.Id) + resolved, err := Resolve(context.Background(), getter, scope) require.NoError(t, err) require.Contains(t, resolved, "db_host") s, err := resolved["db_host"].AsStringValue() require.NoError(t, err) assert.Equal(t, "fallback-host", string(s)) } - -// --------------------------------------------------------------------------- -// Resolve tests — reference to resource config (nested path) -// --------------------------------------------------------------------------- - -func TestResolveValue_Reference_ResourceConfig(t *testing.T) { - scope := newScope() - scope.Resource.Config = map[string]any{ - "networking": map[string]any{ - "vpc_id": "vpc-12345", - }, - } - entity := makeResourceEntity(scope.Resource) - relatedEntity := makeResourceEntity(scope.Resource) - resolver := &mockResolver{ - related: map[string][]*oapi.RelatableEntity{ - "self": {&relatedEntity}, - }, - } - - val := referenceValue("self", "config", "networking", "vpc_id") - lv, err := ResolveValue(context.Background(), resolver, scope.Resource.Id, &entity, &val) - require.NoError(t, err) - s, err := lv.AsStringValue() - require.NoError(t, err) - assert.Equal(t, "vpc-12345", string(s)) -} - -// --------------------------------------------------------------------------- -// Resolve tests — sensitive value returns error -// --------------------------------------------------------------------------- - -func TestResolveValue_Sensitive_ReturnsError(t *testing.T) { - scope := newScope() - v := &oapi.Value{} - _ = v.FromSensitiveValue(oapi.SensitiveValue{ValueHash: "abc123"}) - - entity := makeResourceEntity(scope.Resource) - _, err := ResolveValue(context.Background(), emptyResolver, scope.Resource.Id, &entity, v) - require.Error(t, err) - assert.Contains(t, err.Error(), "sensitive") -} diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/utils.go b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/utils.go index d075e3209..d0fd40378 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/utils.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/utils.go @@ -2,49 +2,35 @@ package variableresolver import ( "fmt" - "reflect" - "strings" "workspace-engine/pkg/oapi" ) -// getPropertyReflection uses reflection to get a property value (fallback method) -func getPropertyReflection(entity any, propertyPath []string) (*oapi.LiteralValue, error) { - if len(propertyPath) == 0 { - return convertValue(entity) - } - - v := reflect.ValueOf(entity) - if v.Kind() == reflect.Ptr { - v = v.Elem() - } - - if v.Kind() != reflect.Struct { - return nil, fmt.Errorf("entity is not a struct") - } - - // Get the first property - fieldName := propertyPath[0] - field := v.FieldByName(fieldName) - if !field.IsValid() { - // Try case-insensitive match - field = v.FieldByNameFunc(func(name string) bool { - return strings.EqualFold(name, fieldName) - }) - if !field.IsValid() { - return nil, fmt.Errorf("field %s not found", fieldName) +// getMapProperty traverses a map[string]any by the given path and converts +// the final value to an oapi.LiteralValue. +func getMapProperty(m map[string]any, path []string) (*oapi.LiteralValue, error) { + var current any = m + for _, key := range path { + switch v := current.(type) { + case map[string]any: + val, ok := v[key] + if !ok { + return nil, fmt.Errorf("key %q not found", key) + } + current = val + case map[string]string: + val, ok := v[key] + if !ok { + return nil, fmt.Errorf("key %q not found", key) + } + current = val + default: + return nil, fmt.Errorf("cannot traverse into %T for key %q", current, key) } } - - // If this is the last element, return the field value - if len(propertyPath) == 1 { - return convertValue(field.Interface()) - } - - return getPropertyReflection(field.Interface(), propertyPath[1:]) + return convertValue(current) } - -// convertValue converts a Go value to an oapi.LiteralValue +// convertValue converts a Go value to an oapi.LiteralValue. func convertValue(val any) (*oapi.LiteralValue, error) { switch v := val.(type) { case *oapi.LiteralValue: @@ -82,7 +68,6 @@ func convertValue(val any) (*oapi.LiteralValue, error) { err := lv.FromObjectValue(oapi.ObjectValue{Object: v}) return lv, err case map[string]string: - // Convert map[string]string to map[string]any m := make(map[string]any, len(v)) for k, val := range v { m[k] = val @@ -98,4 +83,3 @@ func convertValue(val any) (*oapi.LiteralValue, error) { return nil, fmt.Errorf("unexpected variable value type: %T", val) } } - diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/value.go b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/value.go index 055891c24..dd32cb87d 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/value.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/value.go @@ -5,74 +5,78 @@ import ( "fmt" "workspace-engine/pkg/oapi" + "workspace-engine/pkg/workspace/relationships/eval" + + "github.com/charmbracelet/log" + "go.opentelemetry.io/otel/codes" ) -// ResolveValue resolves a single oapi.Value to a concrete LiteralValue. +// relatedLookup resolves a reference name to matched related entities. +type relatedLookup func(ctx context.Context, reference string) (*eval.EntityData, error) + +// resolveValue resolves a single oapi.Value to a concrete LiteralValue. // // Literal values are returned as-is. Reference values are resolved by -// finding related entities through the resolver and traversing the property -// path on the matched entity. Sensitive values are not resolved and return -// an error — they must be handled by a separate decryption path. -func ResolveValue( +// finding related entities through the lookup and traversing the property +// path on the matched entity's raw data. Sensitive values return an error. +func resolveValue( ctx context.Context, - resolver RelatedEntityResolver, - resourceID string, - entity *oapi.RelatableEntity, + lookup relatedLookup, value *oapi.Value, -) (*oapi.LiteralValue, error) { - _, span := tracer.Start(ctx, "variableresolver.ResolveValue") +) *oapi.LiteralValue { + _, span := tracer.Start(ctx, "variableresolver.resolveValue") defer span.End() valueType, err := value.GetType() if err != nil { - return nil, fmt.Errorf("determine value type: %w", err) + span.RecordError(err) + span.SetStatus(codes.Error, "get value type failed") + log.Error("get value type failed", "error", err) + return nil } switch valueType { case "literal": - return resolveLiteral(value) + lv, err := value.AsLiteralValue() + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, "extract literal value failed") + log.Error("extract literal value failed", "error", err) + return nil + } + return &lv case "reference": - return resolveReference(ctx, resolver, value, entity) + lv, err := resolveReference(ctx, lookup, value) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, "resolve reference failed") + log.Error("resolve reference failed", "error", err) + return nil + } + return lv case "sensitive": - return nil, fmt.Errorf("sensitive values are not resolved by the variable resolver") + return nil default: - return nil, fmt.Errorf("unsupported value type: %s", valueType) - } -} - -func resolveLiteral(value *oapi.Value) (*oapi.LiteralValue, error) { - lv, err := value.AsLiteralValue() - if err != nil { - return nil, fmt.Errorf("extract literal value: %w", err) + return nil } - return &lv, nil } func resolveReference( ctx context.Context, - resolver RelatedEntityResolver, + lookup relatedLookup, value *oapi.Value, - entity *oapi.RelatableEntity, ) (*oapi.LiteralValue, error) { rv, err := value.AsReferenceValue() if err != nil { return nil, fmt.Errorf("extract reference value: %w", err) } - refs, err := resolver.ResolveRelated(ctx, rv.Reference) + entity, err := lookup(ctx, rv.Reference) if err != nil { - return nil, fmt.Errorf("resolve related entities for reference %q: %w", rv.Reference, err) - } - if len(refs) == 0 { - return nil, fmt.Errorf( - "reference %q not found for entity %s-%s", - rv.Reference, entity.GetType(), entity.GetID(), - ) + return nil, fmt.Errorf("resolve related for reference %q: %w", rv.Reference, err) } - - lv, err := getPropertyReflection(refs[0].Raw, rv.Path) - if err != nil { - return nil, fmt.Errorf("resolve property path %v on reference %q: %w", rv.Path, rv.Reference, err) + if entity == nil { + return nil, nil } - return lv, nil + return getMapProperty(entity.Raw, rv.Path) } From 7c1f0e3a148bcca4841756f9669f3c1fc910e002 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Thu, 5 Mar 2026 22:59:24 -0800 Subject: [PATCH 05/10] chore: remove failing legacy backfill test --- .../pkg/workspace/store/restore_test.go | 38 ------------------- 1 file changed, 38 deletions(-) diff --git a/apps/workspace-engine/pkg/workspace/store/restore_test.go b/apps/workspace-engine/pkg/workspace/store/restore_test.go index 4a57f644c..300ef7df0 100644 --- a/apps/workspace-engine/pkg/workspace/store/restore_test.go +++ b/apps/workspace-engine/pkg/workspace/store/restore_test.go @@ -1058,44 +1058,6 @@ func TestStore_Restore_DuplicateIDs(t *testing.T) { assert.Equal(t, "docker", restoredResource.Kind) } -func TestStore_Restore_JobReleaseIdMigration(t *testing.T) { - ctx := context.Background() - namespace := "workspace-" + uuid.New().String() - - persistenceStore := memory.NewStore() - - contentHash := "abc123def456abc123def456abc123def456abc123def456abc123def456abcd" - expectedUUID := uuid.NewSHA1(uuid.NameSpaceOID, []byte(contentHash)).String() - - job := &oapi.Job{ - Id: uuid.New().String(), - ReleaseId: contentHash, - Status: oapi.JobStatusSuccessful, - JobAgentId: uuid.New().String(), - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - - changes := persistence.NewChangesBuilder(namespace). - Set(job). - Build() - - err := persistenceStore.Save(ctx, changes) - require.NoError(t, err) - - loadedChanges, err := persistenceStore.Load(ctx, namespace) - require.NoError(t, err) - - testStore := store.New("test-workspace", statechange.NewChangeSet[any]()) - err = testStore.Restore(ctx, loadedChanges, nil) - require.NoError(t, err) - - restoredJob, ok := testStore.Jobs.Get(job.Id) - require.True(t, ok, "Job should be restored") - assert.Equal(t, expectedUUID, restoredJob.ReleaseId, - "Job ReleaseId should be migrated from content hash to deterministic UUID") -} - func TestStore_Restore_JobReleaseIdAlreadyUUID(t *testing.T) { ctx := context.Background() namespace := "workspace-" + uuid.New().String() From 52b4618c2f3a6ec22aa0af42678e5600abda9471 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Thu, 5 Mar 2026 23:04:37 -0800 Subject: [PATCH 06/10] fix: include message in job response --- .../svc/http/server/openapi/deployments/server.go | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/workspace-engine/svc/http/server/openapi/deployments/server.go b/apps/workspace-engine/svc/http/server/openapi/deployments/server.go index c84c959a5..8f04ffd19 100644 --- a/apps/workspace-engine/svc/http/server/openapi/deployments/server.go +++ b/apps/workspace-engine/svc/http/server/openapi/deployments/server.go @@ -398,6 +398,7 @@ func (s *Deployments) GetReleaseTargetsForDeployment(c *gin.Context, workspaceId item.LatestJob = &oapi.JobSummary{ Id: state.LatestJob.Job.Id, Links: &map[string]string{}, + Message: state.LatestJob.Job.Message, Status: state.LatestJob.Job.Status, Verifications: state.LatestJob.Verifications, } From c793461e13238640ea20be6fc43567c79e201eb6 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Fri, 6 Mar 2026 09:26:47 -0800 Subject: [PATCH 07/10] chore: add back migration behind flag --- apps/workspace-engine/pkg/config/env.go | 2 + .../pkg/workspace/store/store.go | 229 ++++++++++++++++++ 2 files changed, 231 insertions(+) diff --git a/apps/workspace-engine/pkg/config/env.go b/apps/workspace-engine/pkg/config/env.go index 717583760..a61354cdf 100644 --- a/apps/workspace-engine/pkg/config/env.go +++ b/apps/workspace-engine/pkg/config/env.go @@ -40,4 +40,6 @@ type Config struct { RegisterAddress string `envconfig:"REGISTER_ADDRESS" default:""` TraceTokenSecret string `envconfig:"TRACE_TOKEN_SECRET" default:"secret"` + + MigrateLegacyEntities bool `envconfig:"MIGRATE_LEGACY_ENTITIES" default:"true"` } diff --git a/apps/workspace-engine/pkg/workspace/store/store.go b/apps/workspace-engine/pkg/workspace/store/store.go index 5eec24113..6c4e53952 100644 --- a/apps/workspace-engine/pkg/workspace/store/store.go +++ b/apps/workspace-engine/pkg/workspace/store/store.go @@ -2,6 +2,7 @@ package store import ( "context" + "workspace-engine/pkg/config" "workspace-engine/pkg/oapi" "workspace-engine/pkg/persistence" "workspace-engine/pkg/selector" @@ -10,6 +11,8 @@ import ( hybridrepo "workspace-engine/pkg/workspace/store/repository/hybrid" "workspace-engine/pkg/workspace/store/repository/memory" + "github.com/charmbracelet/log" + "github.com/google/uuid" "go.opentelemetry.io/otel/attribute" ) @@ -460,6 +463,232 @@ func (s *Store) Restore(ctx context.Context, changes persistence.Changes, setSta // Rebuild in-memory link stores from persisted link entities. s.repo.RestoreLinks() + // Migrate legacy changelog entities into the active repos. + // After Router().Apply(), the in-memory repo may contain entities + // loaded from changelog_entry records. When the DB backend is + // active, sync them so they are available through the DB-backed repos. + if config.Global.MigrateLegacyEntities { + if setStatus != nil { + setStatus("Migrating legacy systems") + } + for _, sys := range s.repo.Systems().Items() { + if err := s.Systems.repo.Set(sys); err != nil { + log.Warn("Failed to migrate legacy system", + "system_id", sys.Id, "name", sys.Name, "error", err) + } + } + + if setStatus != nil { + setStatus("Migrating legacy job agents") + } + for _, ja := range s.repo.JobAgents().Items() { + if err := s.JobAgents.repo.Set(ja); err != nil { + log.Warn("Failed to migrate legacy job agent", + "job_agent_id", ja.Id, "name", ja.Name, "error", err) + } + } + + if setStatus != nil { + setStatus("Migrating legacy deployments") + } + for _, d := range s.repo.Deployments().Items() { + if err := s.Deployments.repo.Set(d); err != nil { + log.Warn("Failed to migrate legacy deployment", + "deployment_id", d.Id, "name", d.Name, "error", err) + } + } + + if setStatus != nil { + setStatus("Migrating legacy environments") + } + for _, env := range s.repo.Environments().Items() { + if err := s.Environments.repo.Set(env); err != nil { + log.Warn("Failed to migrate legacy environment", + "environment_id", env.Id, "name", env.Name, "error", err) + } + } + + if setStatus != nil { + setStatus("Migrating legacy deployment versions") + } + for _, v := range s.repo.DeploymentVersions().Items() { + if err := s.DeploymentVersions.repo.Set(v); err != nil { + log.Warn("Failed to migrate legacy deployment version", + "version_id", v.Id, "name", v.Name, "error", err) + } + } + + if setStatus != nil { + setStatus("Migrating legacy resource providers") + } + for _, rp := range s.repo.ResourceProviders().Items() { + if err := s.ResourceProviders.repo.Set(rp); err != nil { + log.Warn("Failed to migrate legacy resource provider", + "resource_provider_id", rp.Id, "name", rp.Name, "error", err) + } + } + + if setStatus != nil { + setStatus("Migrating legacy resources") + } + for _, r := range s.repo.Resources().Items() { + if err := s.Resources.repo.Set(r); err != nil { + log.Warn("Failed to migrate legacy resource", + "resource_id", r.Id, "name", r.Name, "error", err) + } + } + + if setStatus != nil { + setStatus("Migrating legacy policies") + } + for _, pol := range s.repo.Policies().Items() { + if err := s.Policies.repo.Set(pol); err != nil { + log.Warn("Failed to migrate legacy policy", + "policy_id", pol.Id, "name", pol.Name, "error", err) + } + } + + if setStatus != nil { + setStatus("Migrating legacy policy skips") + } + for _, ps := range s.repo.PolicySkips().Items() { + if err := s.PolicySkips.repo.Set(ps); err != nil { + log.Warn("Failed to migrate legacy policy skip", + "policy_skip_id", ps.Id, "error", err) + } + } + + if setStatus != nil { + setStatus("Migrating legacy user approval records") + } + for _, uar := range s.repo.UserApprovalRecords().Items() { + if err := s.UserApprovalRecords.repo.Set(uar); err != nil { + log.Warn("Failed to migrate legacy user approval record", + "key", uar.Key(), "error", err) + } + } + + if setStatus != nil { + setStatus("Migrating legacy deployment variables") + } + for _, dv := range s.repo.DeploymentVariables().Items() { + if err := s.DeploymentVariables.repo.Set(dv); err != nil { + log.Warn("Failed to migrate legacy deployment variable", + "id", dv.Id, "key", dv.Key, "error", err) + } + } + + if setStatus != nil { + setStatus("Migrating legacy deployment variable values") + } + for _, dvv := range s.repo.DeploymentVariableValues().Items() { + if err := s.DeploymentVariableValues.repo.Set(dvv); err != nil { + log.Warn("Failed to migrate legacy deployment variable value", + "id", dvv.Id, "error", err) + } + } + + if setStatus != nil { + setStatus("Migrating legacy workflows") + } + for _, wf := range s.repo.Workflows().Items() { + if err := s.Workflows.repo.Set(wf); err != nil { + log.Warn("Failed to migrate legacy workflow", + "id", wf.Id, "name", wf.Name, "error", err) + } + } + + if setStatus != nil { + setStatus("Migrating legacy workflow job templates") + } + for _, wjt := range s.repo.WorkflowJobTemplates().Items() { + if err := s.WorkflowJobTemplates.repo.Set(wjt); err != nil { + log.Warn("Failed to migrate legacy workflow job template", + "id", wjt.Id, "name", wjt.Name, "error", err) + } + } + + if setStatus != nil { + setStatus("Migrating legacy workflow runs") + } + for _, wr := range s.repo.WorkflowRuns().Items() { + if err := s.WorkflowRuns.repo.Set(wr); err != nil { + log.Warn("Failed to migrate legacy workflow run", + "id", wr.Id, "error", err) + } + } + + if setStatus != nil { + setStatus("Migrating legacy workflow jobs") + } + for _, wj := range s.repo.WorkflowJobs().Items() { + if err := s.WorkflowJobs.repo.Set(wj); err != nil { + log.Warn("Failed to migrate legacy workflow job", + "id", wj.Id, "error", err) + } + } + + if setStatus != nil { + setStatus("Migrating legacy resource variables") + } + for _, rv := range s.repo.ResourceVariables().Items() { + if err := s.ResourceVariables.repo.Set(rv); err != nil { + log.Warn("Failed to migrate legacy resource variable", + "key", rv.ID(), "error", err) + } + } + + if setStatus != nil { + setStatus("Migrating legacy relationship rules") + } + for _, rr := range s.repo.RelationshipRules.Items() { + if err := s.Relationships.repo.Set(rr); err != nil { + log.Warn("Failed to migrate legacy relationship rule", + "id", rr.Id, "name", rr.Name, "error", err) + } + } + + if setStatus != nil { + setStatus("Migrating legacy releases") + } + for _, rel := range s.repo.Releases().Items() { + if rel.Id == uuid.Nil { + rel.Id = uuid.NewSHA1(uuid.NameSpaceOID, []byte(rel.ContentHash())) + } + if err := s.Releases.repo.Set(rel); err != nil { + log.Warn("Failed to migrate legacy release", + "content_hash", rel.ContentHash(), "error", err) + } + } + + if setStatus != nil { + setStatus("Migrating legacy jobs") + } + for _, job := range s.repo.Jobs.Items() { + if err := s.Jobs.repo.Set(job); err != nil { + log.Warn("Failed to migrate legacy job", + "job_id", job.Id, "error", err) + } + } + + if setStatus != nil { + setStatus("Migrating legacy job release IDs") + } + for _, job := range s.repo.Jobs.Items() { + if job.ReleaseId == "" { + continue + } + if _, err := uuid.Parse(job.ReleaseId); err == nil { + continue + } + job.ReleaseId = uuid.NewSHA1(uuid.NameSpaceOID, []byte(job.ReleaseId)).String() + if err := s.Jobs.repo.Set(job); err != nil { + log.Warn("Failed to migrate legacy job release ID", + "job_id", job.Id, "error", err) + } + } + } + if setStatus != nil { setStatus("Computing release targets") } From 3477a5640b92cc5ebff47d138aa6fbc646811cfe Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Fri, 6 Mar 2026 09:36:03 -0800 Subject: [PATCH 08/10] fix: formatting --- packages/trpc/src/routes/reconcile.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/trpc/src/routes/reconcile.ts b/packages/trpc/src/routes/reconcile.ts index 9184ae221..58c840661 100644 --- a/packages/trpc/src/routes/reconcile.ts +++ b/packages/trpc/src/routes/reconcile.ts @@ -49,9 +49,7 @@ export const reconcileRouter = router({ deploymentId: z.string().uuid(), }), ) - .mutation(({ ctx, input }) => - enqueueDeploymentSelectorEval(ctx.db, input), - ), + .mutation(({ ctx, input }) => enqueueDeploymentSelectorEval(ctx.db, input)), triggerEnvironmentSelectorEval: protectedProcedure .input( From 0b134c9cf7a396be468a802554b1412e2febb83b Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 6 Mar 2026 15:52:22 -0500 Subject: [PATCH 09/10] refactor: consolidate store getters for approval records and streamline evaluator construction --- .../policy/evaluator/approval/approval.go | 8 --- .../policy/evaluator/approval/store.go | 21 ++++++ .../environmentprogression/getters.go | 4 ++ .../gradualrollout/environment_summary.go | 4 +- .../evaluator/gradualrollout/getters.go | 65 +++---------------- .../gradualrollout/gradualrollout.go | 43 +++++++----- .../gradualrollout/gradualrollout_test.go | 64 +++++++++--------- .../policy/evaluator/gradualrollout/store.go | 59 +++++++++++++++++ 8 files changed, 153 insertions(+), 115 deletions(-) create mode 100644 apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/approval/store.go create mode 100644 apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/store.go diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/approval/approval.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/approval/approval.go index f7794c3a5..d90cacd29 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/approval/approval.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/approval/approval.go @@ -52,14 +52,6 @@ type AnyApprovalEvaluator struct { rule *oapi.AnyApprovalRule } -type storeGetters struct { - store *store.Store -} - -func (s *storeGetters) GetApprovalRecords(ctx context.Context, versionID, environmentID string) ([]*oapi.UserApprovalRecord, error) { - return s.store.UserApprovalRecords.GetApprovalRecords(versionID, environmentID), nil -} - func NewEvaluatorFromStore(store *store.Store, approvalRule *oapi.PolicyRule) evaluator.Evaluator { if approvalRule == nil || approvalRule.AnyApproval == nil || store == nil { return nil diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/approval/store.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/approval/store.go new file mode 100644 index 000000000..fb2464bcf --- /dev/null +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/approval/store.go @@ -0,0 +1,21 @@ +package approval + +import ( + "context" + "workspace-engine/pkg/oapi" + "workspace-engine/pkg/workspace/store" +) + +var _ Getters = (*storeGetters)(nil) + +type storeGetters struct { + store *store.Store +} + +func NewStoreGetters(store *store.Store) Getters { + return &storeGetters{store: store} +} + +func (s *storeGetters) GetApprovalRecords(ctx context.Context, versionID, environmentID string) ([]*oapi.UserApprovalRecord, error) { + return s.store.UserApprovalRecords.GetApprovalRecords(versionID, environmentID), nil +} diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/getters.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/getters.go index 7800c0ec6..e334c9cec 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/getters.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/getters.go @@ -39,3 +39,7 @@ func (s *storeGetters) GetJobsForReleaseTarget(releaseTarget *oapi.ReleaseTarget func (s *storeGetters) GetRelease(releaseID string) (*oapi.Release, bool) { return s.store.Releases.Get(releaseID) } + +func NewStoreGetters(store *store.Store) *storeGetters { + return &storeGetters{store: store} +} \ No newline at end of file diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/environment_summary.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/environment_summary.go index d5d46ba6f..29d1e00ea 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/environment_summary.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/environment_summary.go @@ -20,7 +20,7 @@ func NewSummaryEvaluatorFromStore(store *store.Store, rule *oapi.PolicyRule) eva if rule == nil || rule.GradualRollout == nil || store == nil { return nil } - return &GradualRolloutEnvironmentSummaryEvaluator{getters: &storeGetters{store: store}, ruleId: rule.Id, rule: rule.GradualRollout} + return &GradualRolloutEnvironmentSummaryEvaluator{getters: NewStoreGetters(store), ruleId: rule.Id, rule: rule.GradualRollout} } func (e *GradualRolloutEnvironmentSummaryEvaluator) ScopeFields() evaluator.ScopeFields { @@ -110,7 +110,7 @@ func (e *GradualRolloutEnvironmentSummaryEvaluator) Evaluate(ctx context.Context Resource: resource, Deployment: deployment, } - evaluation := e.getters.NewGradualRolloutEvaluator(&oapi.PolicyRule{Id: "gradualRolloutSummary", GradualRollout: e.rule}).Evaluate(ctx, scope) + evaluation := NewEvaluator(e.getters, &oapi.PolicyRule{Id: "gradualRolloutSummary", GradualRollout: e.rule}).Evaluate(ctx, scope) messages = append(messages, evaluation) var targetTime *time.Time diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/getters.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/getters.go index 8fbbe88f3..85c5c2b28 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/getters.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/getters.go @@ -3,71 +3,22 @@ package gradualrollout import ( "context" "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/environmentprogression" - "workspace-engine/pkg/workspace/store" ) +type approvalGetters = approval.Getters +type environmentProgressionGetters = environmentprogression.Getters + type Getters interface { - GetPolicies(ctx context.Context, releaseTarget *oapi.ReleaseTarget) ([]*oapi.Policy, error) + approvalGetters + environmentProgressionGetters + + GetPoliciesForTarget(ctx context.Context, releaseTarget *oapi.ReleaseTarget) ([]*oapi.Policy, error) GetPolicySkips(versionID, environmentID, resourceID string) []*oapi.PolicySkip + HasCurrentRelease(ctx context.Context, releaseTarget *oapi.ReleaseTarget) bool GetResource(resourceID string) (*oapi.Resource, bool) GetDeployment(deploymentID string) (*oapi.Deployment, bool) GetReleaseTargets() ([]*oapi.ReleaseTarget, error) - NewApprovalEvaluator(rule *oapi.PolicyRule) evaluator.Evaluator - NewEnvironmentProgressionEvaluator(rule *oapi.PolicyRule) evaluator.Evaluator - NewGradualRolloutEvaluator(rule *oapi.PolicyRule) evaluator.Evaluator -} - -var _ Getters = (*storeGetters)(nil) - -type storeGetters struct { - store *store.Store -} - -func (s *storeGetters) GetPolicies(ctx context.Context, releaseTarget *oapi.ReleaseTarget) ([]*oapi.Policy, error) { - return s.store.ReleaseTargets.GetPolicies(ctx, releaseTarget) -} - -func (s *storeGetters) GetPolicySkips(versionID, environmentID, resourceID string) []*oapi.PolicySkip { - return s.store.PolicySkips.GetAllForTarget(versionID, environmentID, resourceID) -} - -func (s *storeGetters) HasCurrentRelease(ctx context.Context, releaseTarget *oapi.ReleaseTarget) bool { - _, _, err := s.store.ReleaseTargets.GetCurrentRelease(ctx, releaseTarget) - return err == nil -} - -func (s *storeGetters) GetResource(resourceID string) (*oapi.Resource, bool) { - return s.store.Resources.Get(resourceID) -} - -func (s *storeGetters) GetDeployment(deploymentID string) (*oapi.Deployment, bool) { - return s.store.Deployments.Get(deploymentID) -} - -func (s *storeGetters) GetReleaseTargets() ([]*oapi.ReleaseTarget, error) { - items, err := s.store.ReleaseTargets.Items() - if err != nil { - return nil, err - } - targets := make([]*oapi.ReleaseTarget, 0, len(items)) - for _, rt := range items { - targets = append(targets, rt) - } - return targets, nil -} - -func (s *storeGetters) NewApprovalEvaluator(rule *oapi.PolicyRule) evaluator.Evaluator { - return approval.NewEvaluatorFromStore(s.store, rule) -} - -func (s *storeGetters) NewEnvironmentProgressionEvaluator(rule *oapi.PolicyRule) evaluator.Evaluator { - return environmentprogression.NewEvaluatorFromStore(s.store, rule) -} - -func (s *storeGetters) NewGradualRolloutEvaluator(rule *oapi.PolicyRule) evaluator.Evaluator { - return NewEvaluatorFromStore(s.store, rule) } diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/gradualrollout.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/gradualrollout.go index e9425b5a7..d989845d3 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/gradualrollout.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/gradualrollout.go @@ -8,7 +8,9 @@ import ( "time" "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/results" "workspace-engine/pkg/workspace/store" ) @@ -56,7 +58,7 @@ func NewEvaluatorFromStore(store *store.Store, rolloutRule *oapi.PolicyRule) eva } return evaluator.WithMemoization(&GradualRolloutEvaluator{ - getters: &storeGetters{store: store}, + getters: NewStoreGetters(store), ruleId: rolloutRule.Id, rule: rolloutRule.GradualRollout, hashingFn: fnvHashingFn, @@ -64,6 +66,23 @@ func NewEvaluatorFromStore(store *store.Store, rolloutRule *oapi.PolicyRule) eva }) } +func NewEvaluator(getters Getters, rolloutRule *oapi.PolicyRule) *GradualRolloutEvaluator { + timeGetter := func() time.Time { + return time.Now() + } + if testTimeGetterFactory != nil { + timeGetter = testTimeGetterFactory + } + + return &GradualRolloutEvaluator{ + getters: getters, + ruleId: rolloutRule.Id, + rule: rolloutRule.GradualRollout, + hashingFn: fnvHashingFn, + timeGetter: timeGetter, + } +} + // ScopeFields declares that this evaluator cares about Environment, Version, and ReleaseTarget. func (e *GradualRolloutEvaluator) ScopeFields() evaluator.ScopeFields { return evaluator.ScopeEnvironment | evaluator.ScopeVersion | evaluator.ScopeReleaseTarget @@ -107,11 +126,7 @@ func (e *GradualRolloutEvaluator) getStartTimeFromApprovalRule(ctx context.Conte return &scope.Version.CreatedAt } - approvalEvaluator := e.getters.NewApprovalEvaluator(rule) - if approvalEvaluator == nil { - return nil - } - + approvalEvaluator := approval.NewEvaluator(e.getters, rule) result := approvalEvaluator.Evaluate(ctx, scope) if !result.Allowed || result.SatisfiedAt == nil { return nil @@ -145,11 +160,7 @@ func (e *GradualRolloutEvaluator) getStartTimeFromEnvironmentProgressionRule(ctx return &scope.Version.CreatedAt } - environmentProgressionEvaluator := e.getters.NewEnvironmentProgressionEvaluator(rule) - if environmentProgressionEvaluator == nil { - return nil - } - + environmentProgressionEvaluator := environmentprogression.NewEvaluator(e.getters, rule) result := environmentProgressionEvaluator.Evaluate(ctx, scope) if !result.Allowed || result.SatisfiedAt == nil { return nil @@ -165,11 +176,6 @@ func (e *GradualRolloutEvaluator) getRolloutStartTime(ctx context.Context, envir // - deployment window rules: // - allow windows: rollout starts when window opens (if outside) // - deny windows: rollout starts when window ends (if inside) - policiesForTarget, err := e.getters.GetPolicies(ctx, releaseTarget) - if err != nil { - return nil, err - } - var approvalSatisfiedAt *time.Time var foundApprovalPolicy bool @@ -186,6 +192,11 @@ func (e *GradualRolloutEvaluator) getRolloutStartTime(ctx context.Context, envir allSkips := e.getters.GetPolicySkips(version.Id, environment.Id, releaseTarget.ResourceId) + policiesForTarget, err := e.getters.GetPoliciesForTarget(ctx, releaseTarget) + if err != nil { + return nil, err + } + for _, policy := range policiesForTarget { if !policy.Enabled { continue diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/gradualrollout_test.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/gradualrollout_test.go index c0122552f..c984e4dea 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/gradualrollout_test.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/gradualrollout_test.go @@ -173,7 +173,7 @@ func TestGradualRolloutEvaluator_LinearRollout(t *testing.T) { rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -253,7 +253,7 @@ func TestGradualRolloutEvaluator_LinearRollout_Pending(t *testing.T) { rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -335,7 +335,7 @@ func TestGradualRolloutEvaluator_LinearNormalizedRollout(t *testing.T) { rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinearNormalized, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -415,7 +415,7 @@ func TestGradualRolloutEvaluator_ZeroTimeScaleIntervalStartsImmediately(t *testi rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 0) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -461,7 +461,7 @@ func TestGradualRolloutEvaluator_UnsatisfiedApprovalRequirement(t *testing.T) { rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -548,7 +548,7 @@ func TestGradualRolloutEvaluator_SatisfiedApprovalRequirement(t *testing.T) { rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -669,7 +669,7 @@ func TestGradualRolloutEvaluator_IfApprovalPolicySkipped_RolloutStartsImmediatel rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -776,7 +776,7 @@ func TestGradualRolloutEvaluator_IfEnvironmentProgressionPolicySkipped_RolloutSt rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -898,7 +898,7 @@ func TestGradualRolloutEvaluator_EnvironmentProgressionOnly_SuccessPercentage(t rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -1021,7 +1021,7 @@ func TestGradualRolloutEvaluator_EnvironmentProgressionOnly_SoakTime(t *testing. rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -1133,7 +1133,7 @@ func TestGradualRolloutEvaluator_EnvironmentProgressionOnly_BothSuccessPercentag rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -1265,7 +1265,7 @@ func TestGradualRolloutEvaluator_EnvironmentProgressionOnly_Unsatisfied(t *testi rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), rule: rule.GradualRollout, ruleId: rule.Id, hashingFn: hashingFn, @@ -1352,7 +1352,7 @@ func TestGradualRolloutEvaluator_BothPolicies_BothSatisfied(t *testing.T) { rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -1493,7 +1493,7 @@ func TestGradualRolloutEvaluator_BothPolicies_ApprovalLater(t *testing.T) { rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -1633,7 +1633,7 @@ func TestGradualRolloutEvaluator_BothPolicies_ApprovalUnsatisfied(t *testing.T) rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -1777,7 +1777,7 @@ func TestGradualRolloutEvaluator_BothPolicies_EnvProgUnsatisfied(t *testing.T) { rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -1883,7 +1883,7 @@ func TestGradualRolloutEvaluator_ApprovalJustSatisfied_OnlyPosition0Allowed(t *t rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -1985,7 +1985,7 @@ func TestGradualRolloutEvaluator_GradualProgressionOverTime(t *testing.T) { rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) // 60 seconds between each position eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), rule: rule.GradualRollout, ruleId: rule.Id, hashingFn: hashingFn, @@ -2112,7 +2112,7 @@ func TestGradualRolloutEvaluator_EnvProgressionJustSatisfied_OnlyPosition0Allowe rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), rule: rule.GradualRollout, ruleId: rule.Id, hashingFn: hashingFn, @@ -2239,7 +2239,7 @@ func TestGradualRolloutEvaluator_NextEvaluationTime_WhenPending(t *testing.T) { rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) // 60 seconds between deployments eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -2305,7 +2305,7 @@ func TestGradualRolloutEvaluator_NextEvaluationTime_WhenAllowed(t *testing.T) { rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -2363,7 +2363,7 @@ func TestGradualRolloutEvaluator_NextEvaluationTime_WaitingForDependencies(t *te rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -2456,7 +2456,7 @@ func TestGradualRolloutEvaluator_EnvironmentProgressionNoReleaseTargets(t *testi rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -2567,7 +2567,7 @@ func TestGradualRolloutEvaluator_NextEvaluationTime_LinearNormalized(t *testing. rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinearNormalized, 120) // Total 120 seconds for all eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -2643,7 +2643,7 @@ func TestGradualRolloutEvaluator_DeploymentWindow_InsideAllowWindow(t *testing.T rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -2727,7 +2727,7 @@ func TestGradualRolloutEvaluator_DeploymentWindow_OutsideAllowWindow(t *testing. rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -2815,7 +2815,7 @@ func TestGradualRolloutEvaluator_DeploymentWindow_IgnoresWindowWithoutDeployedVe rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -2886,7 +2886,7 @@ func TestGradualRolloutEvaluator_DeploymentWindow_DenyWindowAdjustsRolloutStart( rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -2965,7 +2965,7 @@ func TestGradualRolloutEvaluator_DeploymentWindow_DenyWindowOutsideNoChange(t *t rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -3047,7 +3047,7 @@ func TestGradualRolloutEvaluator_DeploymentWindow_DenyWindowPreventsFrontloading // 60 second intervals between deployments rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -3145,7 +3145,7 @@ func TestGradualRolloutEvaluator_DeploymentWindow_NoWindowsExistingBehavior(t *t rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, @@ -3224,7 +3224,7 @@ func TestGradualRolloutEvaluator_DeploymentWindow_PreventsFrontloading(t *testin // 60 second intervals between deployments rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := GradualRolloutEvaluator{ - getters: &storeGetters{store: st}, + getters: NewStoreGetters(st), ruleId: rule.Id, rule: rule.GradualRollout, hashingFn: hashingFn, diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/store.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/store.go new file mode 100644 index 000000000..610417fe8 --- /dev/null +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/store.go @@ -0,0 +1,59 @@ +package gradualrollout + +import ( + "context" + "workspace-engine/pkg/oapi" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/approval" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression" + "workspace-engine/pkg/workspace/store" +) + +var _ Getters = (*storeGetters)(nil) + +type storeGetters struct { + approvalGetters + environmentProgressionGetters + store *store.Store +} + +func NewStoreGetters(store *store.Store) *storeGetters { + return &storeGetters{ + store: store, + approvalGetters: approval.NewStoreGetters(store), + environmentProgressionGetters: environmentprogression.NewStoreGetters(store), + } +} + +// GetPoliciesForTarget implements [Getters]. +func (s *storeGetters) GetPoliciesForTarget(ctx context.Context, releaseTarget *oapi.ReleaseTarget) ([]*oapi.Policy, error) { + return s.store.ReleaseTargets.GetPolicies(ctx, releaseTarget) +} + +func (s *storeGetters) GetPolicySkips(versionID, environmentID, resourceID string) []*oapi.PolicySkip { + return s.store.PolicySkips.GetAllForTarget(versionID, environmentID, resourceID) +} + +func (s *storeGetters) HasCurrentRelease(ctx context.Context, releaseTarget *oapi.ReleaseTarget) bool { + _, _, err := s.store.ReleaseTargets.GetCurrentRelease(ctx, releaseTarget) + return err == nil +} + +func (s *storeGetters) GetResource(resourceID string) (*oapi.Resource, bool) { + return s.store.Resources.Get(resourceID) +} + +func (s *storeGetters) GetDeployment(deploymentID string) (*oapi.Deployment, bool) { + return s.store.Deployments.Get(deploymentID) +} + +func (s *storeGetters) GetReleaseTargets() ([]*oapi.ReleaseTarget, error) { + items, err := s.store.ReleaseTargets.Items() + if err != nil { + return nil, err + } + targets := make([]*oapi.ReleaseTarget, 0, len(items)) + for _, rt := range items { + targets = append(targets, rt) + } + return targets, nil +} From cd09aa1cfd229ba56066b532b4baef7317b18a16 Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 6 Mar 2026 16:14:04 -0500 Subject: [PATCH 10/10] refactor: update getters to return errors and enhance error handling in evaluators --- .../convert => pkg/db}/convert.go | 23 ++++---- .../pkg/getters/deployment.go | 50 +++++++++++++++++ .../pkg/getters/environment.go | 50 +++++++++++++++++ apps/workspace-engine/pkg/getters/resource.go | 50 +++++++++++++++++ .../policy/evaluator/approval/postgres.go | 33 ++++++++++++ .../deployableversions/deployableversions.go | 8 ++- .../evaluator/deployableversions/getters.go | 6 +-- .../evaluator/deploymentdependency/getters.go | 28 ++++++++++ .../deploymentwindow/deploymentwindow.go | 7 ++- .../evaluator/deploymentwindow/getters.go | 32 +++++++++-- .../evaluator/versioncooldown/getters.go | 50 ++++++++++++----- .../desiredrelease/policyeval/getter.go | 54 +++++++++++++++++++ .../policyeval/getter_postgres.go | 19 +++++++ .../desiredrelease/policyeval/policyeval.go | 15 +----- 14 files changed, 380 insertions(+), 45 deletions(-) rename apps/workspace-engine/{svc/controllers/desiredrelease/convert => pkg/db}/convert.go (85%) create mode 100644 apps/workspace-engine/pkg/getters/deployment.go create mode 100644 apps/workspace-engine/pkg/getters/environment.go create mode 100644 apps/workspace-engine/pkg/getters/resource.go create mode 100644 apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/approval/postgres.go create mode 100644 apps/workspace-engine/svc/controllers/desiredrelease/policyeval/getter.go create mode 100644 apps/workspace-engine/svc/controllers/desiredrelease/policyeval/getter_postgres.go diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/convert/convert.go b/apps/workspace-engine/pkg/db/convert.go similarity index 85% rename from apps/workspace-engine/svc/controllers/desiredrelease/convert/convert.go rename to apps/workspace-engine/pkg/db/convert.go index e20da53eb..72ffdcd65 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/convert/convert.go +++ b/apps/workspace-engine/pkg/db/convert.go @@ -1,15 +1,14 @@ -package convert +package db import ( "encoding/json" - "workspace-engine/pkg/db" "workspace-engine/pkg/oapi" "github.com/google/uuid" ) -func Deployment(row db.Deployment) *oapi.Deployment { +func ToOapiDeployment(row Deployment) *oapi.Deployment { d := &oapi.Deployment{ Id: row.ID.String(), Name: row.Name, @@ -26,7 +25,7 @@ func Deployment(row db.Deployment) *oapi.Deployment { return d } -func Environment(row db.Environment) *oapi.Environment { +func ToOapiEnvironment(row Environment) *oapi.Environment { e := &oapi.Environment{ Id: row.ID.String(), Name: row.Name, @@ -41,7 +40,7 @@ func Environment(row db.Environment) *oapi.Environment { return e } -func Resource(row db.GetResourceByIDRow) *oapi.Resource { +func ToOapiResource(row GetResourceByIDRow) *oapi.Resource { r := &oapi.Resource{ Id: row.ID.String(), Name: row.Name, @@ -70,7 +69,7 @@ func Resource(row db.GetResourceByIDRow) *oapi.Resource { return r } -func Policy(row db.Policy) *oapi.Policy { +func ToOapiPolicy(row Policy) *oapi.Policy { p := &oapi.Policy{ Id: row.ID.String(), Name: row.Name, @@ -89,7 +88,7 @@ func Policy(row db.Policy) *oapi.Policy { return p } -func UserApprovalRecord(row db.UserApprovalRecord) *oapi.UserApprovalRecord { +func ToOapiUserApprovalRecord(row UserApprovalRecord) *oapi.UserApprovalRecord { r := &oapi.UserApprovalRecord{ VersionId: row.VersionID.String(), UserId: row.UserID.String(), @@ -105,7 +104,7 @@ func UserApprovalRecord(row db.UserApprovalRecord) *oapi.UserApprovalRecord { return r } -func PolicySkip(row db.PolicySkip) *oapi.PolicySkip { +func ToOapiPolicySkip(row PolicySkip) *oapi.PolicySkip { s := &oapi.PolicySkip{ Id: row.ID.String(), CreatedBy: row.CreatedBy, @@ -131,7 +130,7 @@ func PolicySkip(row db.PolicySkip) *oapi.PolicySkip { return s } -func DeploymentVariable(row db.DeploymentVariable) oapi.DeploymentVariable { +func ToOapiDeploymentVariable(row DeploymentVariable) oapi.DeploymentVariable { v := oapi.DeploymentVariable{ Id: row.ID.String(), DeploymentId: row.DeploymentID.String(), @@ -149,7 +148,7 @@ func DeploymentVariable(row db.DeploymentVariable) oapi.DeploymentVariable { return v } -func DeploymentVariableValue(row db.DeploymentVariableValue) oapi.DeploymentVariableValue { +func ToOapiDeploymentVariableValue(row DeploymentVariableValue) oapi.DeploymentVariableValue { v := oapi.DeploymentVariableValue{ Id: row.ID.String(), DeploymentVariableId: row.DeploymentVariableID.String(), @@ -167,7 +166,7 @@ func DeploymentVariableValue(row db.DeploymentVariableValue) oapi.DeploymentVari return v } -func ResourceVariable(row db.ResourceVariable) oapi.ResourceVariable { +func ToOapiResourceVariable(row ResourceVariable) oapi.ResourceVariable { v := oapi.ResourceVariable{ ResourceId: row.ResourceID.String(), Key: row.Key, @@ -178,7 +177,7 @@ func ResourceVariable(row db.ResourceVariable) oapi.ResourceVariable { return v } -func DeploymentVersion(row db.DeploymentVersion) *oapi.DeploymentVersion { +func ToOapiDeploymentVersion(row DeploymentVersion) *oapi.DeploymentVersion { v := &oapi.DeploymentVersion{ Id: row.ID.String(), Name: row.Name, diff --git a/apps/workspace-engine/pkg/getters/deployment.go b/apps/workspace-engine/pkg/getters/deployment.go new file mode 100644 index 000000000..c07217ff1 --- /dev/null +++ b/apps/workspace-engine/pkg/getters/deployment.go @@ -0,0 +1,50 @@ +package getters + +import ( + "context" + "fmt" + "workspace-engine/pkg/db" + "workspace-engine/pkg/oapi" + "workspace-engine/pkg/workspace/store" + + "github.com/google/uuid" +) + +type DeploymentGetter interface { + GetDeployment(ctx context.Context, deploymentID string) (*oapi.Deployment, error) +} + +var _ DeploymentGetter = (*PostgresDeploymentGetter)(nil) + +type PostgresDeploymentGetter struct { + queries *db.Queries +} + +func NewPostgresDeploymentGetter(queries *db.Queries) *PostgresDeploymentGetter { + return &PostgresDeploymentGetter{queries: queries} +} + +// GetDeployment implements [DeploymentGetter]. +func (d *PostgresDeploymentGetter) GetDeployment(ctx context.Context, deploymentID string) (*oapi.Deployment, error) { + deployment, err := d.queries.GetDeploymentByID(ctx, uuid.MustParse(deploymentID)) + if err != nil { + return nil, err + } + return db.ToOapiDeployment(deployment), nil +} + +type StoreDeploymentGetter struct { + store *store.Store +} + +func NewStoreDeploymentGetter(store *store.Store) *StoreDeploymentGetter { + return &StoreDeploymentGetter{store: store} +} + +func (s *StoreDeploymentGetter) GetDeployment(ctx context.Context, deploymentID string) (*oapi.Deployment, error) { + deployment, ok := s.store.Deployments.Get(deploymentID) + if !ok { + return nil, fmt.Errorf("deployment not found") + } + return deployment, nil +} \ No newline at end of file diff --git a/apps/workspace-engine/pkg/getters/environment.go b/apps/workspace-engine/pkg/getters/environment.go new file mode 100644 index 000000000..8e69f1905 --- /dev/null +++ b/apps/workspace-engine/pkg/getters/environment.go @@ -0,0 +1,50 @@ +package getters + +import ( + "context" + "fmt" + "workspace-engine/pkg/db" + "workspace-engine/pkg/oapi" + "workspace-engine/pkg/workspace/store" + + "github.com/google/uuid" +) + +type EnvironmentGetter interface { + GetEnvironment(ctx context.Context, environmentID string) (*oapi.Environment, error) +} + +var _ EnvironmentGetter = (*PostgresEnvironmentGetter)(nil) + +type PostgresEnvironmentGetter struct { + queries *db.Queries +} + +func NewPostgresEnvironmentGetter(queries *db.Queries) *PostgresEnvironmentGetter { + return &PostgresEnvironmentGetter{queries: queries} +} + +// GetEnvironment implements [EnvironmentGetter]. +func (e *PostgresEnvironmentGetter) GetEnvironment(ctx context.Context, environmentID string) (*oapi.Environment, error) { + environment, err := e.queries.GetEnvironmentByID(ctx, uuid.MustParse(environmentID)) + if err != nil { + return nil, err + } + return db.ToOapiEnvironment(environment), nil +} + +type StoreEnvironmentGetter struct { + store *store.Store +} + +func NewStoreEnvironmentGetter(store *store.Store) *StoreEnvironmentGetter { + return &StoreEnvironmentGetter{store: store} +} + +func (s *StoreEnvironmentGetter) GetEnvironment(ctx context.Context, environmentID string) (*oapi.Environment, error) { + environment, ok := s.store.Environments.Get(environmentID) + if !ok { + return nil, fmt.Errorf("environment not found") + } + return environment, nil +} \ No newline at end of file diff --git a/apps/workspace-engine/pkg/getters/resource.go b/apps/workspace-engine/pkg/getters/resource.go new file mode 100644 index 000000000..391f891c7 --- /dev/null +++ b/apps/workspace-engine/pkg/getters/resource.go @@ -0,0 +1,50 @@ +package getters + +import ( + "context" + "fmt" + "workspace-engine/pkg/db" + "workspace-engine/pkg/oapi" + "workspace-engine/pkg/workspace/store" + + "github.com/google/uuid" +) + +type ResourceGetter interface { + GetResource(ctx context.Context, resourceID string) (*oapi.Resource, error) +} + +var _ ResourceGetter = (*PostgresResourceGetter)(nil) + +type PostgresResourceGetter struct { + queries *db.Queries +} + +func NewPostgresResourceGetter(queries *db.Queries) *PostgresResourceGetter { + return &PostgresResourceGetter{queries: queries} +} + +// GetResource implements [ResourceGetter]. +func (r *PostgresResourceGetter) GetResource(ctx context.Context, resourceID string) (*oapi.Resource, error) { + resource, err := r.queries.GetResourceByID(ctx, uuid.MustParse(resourceID)) + if err != nil { + return nil, err + } + return db.ToOapiResource(resource), nil +} + +type StoreResourceGetter struct { + store *store.Store +} + +func NewStoreResourceGetter(store *store.Store) *StoreResourceGetter { + return &StoreResourceGetter{store: store} +} + +func (s *StoreResourceGetter) GetResource(ctx context.Context, resourceID string) (*oapi.Resource, error) { + resource, ok := s.store.Resources.Get(resourceID) + if !ok { + return nil, fmt.Errorf("resource not found") + } + return resource, nil +} \ No newline at end of file diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/approval/postgres.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/approval/postgres.go new file mode 100644 index 000000000..b65922f08 --- /dev/null +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/approval/postgres.go @@ -0,0 +1,33 @@ +package approval + +import ( + "context" + "fmt" + "workspace-engine/pkg/db" + "workspace-engine/pkg/oapi" + + "github.com/google/uuid" +) + +type postgresGetters struct { + queries *db.Queries +} + +func NewPostgresGetters(queries *db.Queries) *postgresGetters { + return &postgresGetters{queries: queries} +} + +func (g *postgresGetters) GetApprovalRecords(ctx context.Context, versionID, environmentID string) ([]*oapi.UserApprovalRecord, error) { + approvalRecords, err := db.GetQueries(ctx).ListApprovedRecordsByVersionAndEnvironment(ctx, db.ListApprovedRecordsByVersionAndEnvironmentParams{ + VersionID: uuid.MustParse(versionID), + EnvironmentID: uuid.MustParse(environmentID), + }) + if err != nil { + return nil, fmt.Errorf("list approval records for workspace %s: %w", versionID, err) + } + approvalRecordsOAPI := make([]*oapi.UserApprovalRecord, 0, len(approvalRecords)) + for _, approvalRecord := range approvalRecords { + approvalRecordsOAPI = append(approvalRecordsOAPI, db.ToOapiUserApprovalRecord(approvalRecord)) + } + return approvalRecordsOAPI, nil +} diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deployableversions/deployableversions.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deployableversions/deployableversions.go index 42b415f28..451500c0d 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deployableversions/deployableversions.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deployableversions/deployableversions.go @@ -68,7 +68,13 @@ func (e *DeployableVersionStatusEvaluator) Evaluate( // Paused versions are "grandfathered in" - they can continue on targets // where they're already deployed, but cannot deploy to new targets. // Check if this paused version has an existing release for this target. - releases := e.getters.GetReleases() + releases, err := e.getters.GetReleases() + if err != nil { + return results.NewDeniedResult("Failed to get releases"). + WithDetail("version_id", version.Id). + WithDetail("version_status", version.Status). + WithDetail("error", err.Error()) + } for _, release := range releases { if release == nil { continue diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deployableversions/getters.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deployableversions/getters.go index 11315cab9..a33883f95 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deployableversions/getters.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deployableversions/getters.go @@ -6,7 +6,7 @@ import ( ) type Getters interface { - GetReleases() map[string]*oapi.Release + GetReleases() (map[string]*oapi.Release, error) } var _ Getters = (*storeGetters)(nil) @@ -15,6 +15,6 @@ type storeGetters struct { store *store.Store } -func (s *storeGetters) GetReleases() map[string]*oapi.Release { - return s.store.Releases.Items() +func (s *storeGetters) GetReleases() (map[string]*oapi.Release, error) { + return s.store.Releases.Items(), nil } diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentdependency/getters.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentdependency/getters.go index 7b22a76ef..ebe83df59 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentdependency/getters.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentdependency/getters.go @@ -2,8 +2,11 @@ package deploymentdependency import ( "context" + "workspace-engine/pkg/db" "workspace-engine/pkg/oapi" "workspace-engine/pkg/workspace/store" + + "github.com/google/uuid" ) type Getters interface { @@ -29,3 +32,28 @@ func (s *storeGetters) GetReleaseTargetsForResource(ctx context.Context, resourc func (s *storeGetters) GetLatestCompletedJobForReleaseTarget(releaseTarget *oapi.ReleaseTarget) *oapi.Job { return s.store.Jobs.GetLatestCompletedJobForReleaseTarget(releaseTarget) } + +var _ Getters = (*postgresGetters)(nil) + +type postgresGetters struct { + queries *db.Queries + workspaceID string +} + +func NewPostgresGetters(wsID string, queries *db.Queries) *postgresGetters { + return &postgresGetters{workspaceID: wsID, queries: queries} +} + +func (g *postgresGetters) GetDeployments() (map[string]*oapi.Deployment, error) { + deployments, err := g.queries.ListDeploymentsByWorkspaceID(context.Background(), db.ListDeploymentsByWorkspaceIDParams{ + WorkspaceID: uuid.MustParse(g.workspaceID), + }) + if err != nil { + return make(map[string]*oapi.Deployment), err + } + deploymentsOAPI := make(map[string]*oapi.Deployment, len(deployments)) + for _, deployment := range deployments { + deploymentsOAPI[deployment.ID.String()] = db.ToOapiDeployment(deployment) + } + return deploymentsOAPI, nil +} diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentwindow/deploymentwindow.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentwindow/deploymentwindow.go index e070d9f2f..b4e5274d8 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentwindow/deploymentwindow.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentwindow/deploymentwindow.go @@ -117,7 +117,12 @@ func (e *DeploymentWindowEvaluator) Evaluate( _, span := tracer.Start(ctx, "DeploymentWindowEvaluator.Evaluate") defer span.End() - if !e.getters.HasCurrentRelease(ctx, scope.ReleaseTarget()) { + hasCurrentRelease, err := e.getters.HasCurrentRelease(ctx, scope.ReleaseTarget()) + if err != nil { + return results.NewDeniedResult("Failed to check if current release exists"). + WithDetail("error", err.Error()) + } + if !hasCurrentRelease { return results.NewAllowedResult("No previous version deployed - deployment window ignored"). WithDetail("reason", "first_deployment") } diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentwindow/getters.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentwindow/getters.go index 531ba788d..22467fef2 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentwindow/getters.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentwindow/getters.go @@ -2,12 +2,16 @@ package deploymentwindow import ( "context" + "fmt" + "workspace-engine/pkg/db" "workspace-engine/pkg/oapi" "workspace-engine/pkg/workspace/store" + + "github.com/google/uuid" ) type Getters interface { - HasCurrentRelease(ctx context.Context, releaseTarget *oapi.ReleaseTarget) bool + HasCurrentRelease(ctx context.Context, releaseTarget *oapi.ReleaseTarget) (bool, error) } var _ Getters = (*storeGetters)(nil) @@ -16,7 +20,29 @@ type storeGetters struct { store *store.Store } -func (s *storeGetters) HasCurrentRelease(ctx context.Context, releaseTarget *oapi.ReleaseTarget) bool { +func (s *storeGetters) HasCurrentRelease(ctx context.Context, releaseTarget *oapi.ReleaseTarget) (bool, error) { _, _, err := s.store.ReleaseTargets.GetCurrentRelease(ctx, releaseTarget) - return err == nil + return err == nil, nil +} + +var _ Getters = (*postgresGetters)(nil) + +type postgresGetters struct { + queries *db.Queries } + +func NewPostgresGetters(queries *db.Queries) *postgresGetters { + return &postgresGetters{queries: queries} +} + +func (g *postgresGetters) HasCurrentRelease(ctx context.Context, releaseTarget *oapi.ReleaseTarget) (bool, error) { + releases, err := db.GetQueries(ctx).ListReleasesByReleaseTarget(ctx, db.ListReleasesByReleaseTargetParams{ + ResourceID: uuid.MustParse(releaseTarget.ResourceId), + EnvironmentID: uuid.MustParse(releaseTarget.EnvironmentId), + DeploymentID: uuid.MustParse(releaseTarget.DeploymentId), + }) + if err != nil { + return false, fmt.Errorf("list releases for release target: %w", err) + } + return len(releases) > 0, nil +} \ No newline at end of file diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versioncooldown/getters.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versioncooldown/getters.go index 039f15400..7d34a9cc0 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versioncooldown/getters.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versioncooldown/getters.go @@ -1,28 +1,47 @@ package versioncooldown import ( + "workspace-engine/pkg/db" + "workspace-engine/pkg/db/getters" "workspace-engine/pkg/oapi" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator" "workspace-engine/pkg/workspace/store" ) +type deploymentGetter = getters.DeploymentGetter +type environmentGetter = getters.EnvironmentGetter +type resourceGetter = getters.ResourceGetter + type Getters interface { + deploymentGetter + environmentGetter + resourceGetter + GetJobsForReleaseTarget(releaseTarget *oapi.ReleaseTarget) map[string]*oapi.Job GetRelease(releaseID string) (*oapi.Release, bool) GetJobVerificationStatus(jobID string) oapi.JobVerificationStatus GetReleaseTargets() ([]*oapi.ReleaseTarget, error) - GetEnvironment(environmentID string) (*oapi.Environment, bool) - GetResource(resourceID string) (*oapi.Resource, bool) - GetDeployment(deploymentID string) (*oapi.Deployment, bool) NewVersionCooldownEvaluator(rule *oapi.PolicyRule) evaluator.Evaluator } var _ Getters = (*storeGetters)(nil) type storeGetters struct { + deploymentGetter + environmentGetter + resourceGetter store *store.Store } +func NewStoreGetters(store *store.Store) *storeGetters { + return &storeGetters{ + deploymentGetter: getters.NewStoreDeploymentGetter(store), + environmentGetter: getters.NewStoreEnvironmentGetter(store), + resourceGetter: getters.NewStoreResourceGetter(store), + store: store, + } +} + func (s *storeGetters) GetJobsForReleaseTarget(releaseTarget *oapi.ReleaseTarget) map[string]*oapi.Job { return s.store.Jobs.GetJobsForReleaseTarget(releaseTarget) } @@ -47,18 +66,25 @@ func (s *storeGetters) GetReleaseTargets() ([]*oapi.ReleaseTarget, error) { return targets, nil } -func (s *storeGetters) GetEnvironment(environmentID string) (*oapi.Environment, bool) { - return s.store.Environments.Get(environmentID) +func (s *storeGetters) NewVersionCooldownEvaluator(rule *oapi.PolicyRule) evaluator.Evaluator { + return NewEvaluatorFromStore(s.store, rule) } -func (s *storeGetters) GetResource(resourceID string) (*oapi.Resource, bool) { - return s.store.Resources.Get(resourceID) -} +var _ Getters = (*postgresGetters)(nil) + -func (s *storeGetters) GetDeployment(deploymentID string) (*oapi.Deployment, bool) { - return s.store.Deployments.Get(deploymentID) +type postgresGetters struct { + queries *db.Queries + deploymentGetter deploymentGetter + environmentGetter environmentGetter + resourceGetter resourceGetter } -func (s *storeGetters) NewVersionCooldownEvaluator(rule *oapi.PolicyRule) evaluator.Evaluator { - return NewEvaluatorFromStore(s.store, rule) +func NewPostgresGetters(queries *db.Queries) *postgresGetters { + return &postgresGetters{ + queries: queries, + deploymentGetter: getters.NewPostgresDeploymentGetter(queries), + environmentGetter: getters.NewPostgresEnvironmentGetter(queries), + resourceGetter: getters.NewPostgresResourceGetter(queries), + } } diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/getter.go b/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/getter.go new file mode 100644 index 000000000..9075ea1a8 --- /dev/null +++ b/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/getter.go @@ -0,0 +1,54 @@ +package policyeval + +import ( + "workspace-engine/pkg/db" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/approval" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deployableversions" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentdependency" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentwindow" + "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 gradualrolloutGetters = gradualrollout.Getters +type approvalGetters = approval.Getters +type environmentprogressionGetters = environmentprogression.Getters +type deploymentwindowGetters = deploymentwindow.Getters +type deploymentdependencyGetters = deploymentdependency.Getters +type versioncooldownGetters = versioncooldown.Getters +type deployableversionsGetters = deployableversions.Getters + +// Getter provides the data-access methods needed by policy evaluators. +type Getter interface { + approvalGetters + environmentprogressionGetters + deploymentwindowGetters + deploymentdependencyGetters + versioncooldownGetters + deployableversionsGetters + gradualrolloutGetters +} + +type postgresGetter struct { + approvalGetters + environmentprogressionGetters + deploymentwindowGetters + deploymentdependencyGetters + versioncooldownGetters + deployableversionsGetters + gradualrolloutGetters +} + +func NewGetter(wsID string, queries *db.Queries) *postgresGetter { + return &postgresGetter{ + approvalGetters: approval.NewPostgresGetters(queries), + deploymentwindowGetters: deploymentwindow.NewPostgresGetters(queries), + + environmentprogressionGetters: environmentprogression.NewPostgresGetters(queries), + deploymentdependencyGetters: deploymentdependency.NewPostgresGetters(wsID, queries), + versioncooldownGetters: versioncooldown.NewPostgresGetters(queries), + deployableversionsGetters: deployableversions.NewPostgresGetters(wsID, queries), + gradualrolloutGetters: gradualrollout.NewPostgresGetters(wsID, queries), + } +} \ No newline at end of file diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/getter_postgres.go b/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/getter_postgres.go new file mode 100644 index 000000000..de4239a7a --- /dev/null +++ b/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/getter_postgres.go @@ -0,0 +1,19 @@ +package policyeval + +import ( + "workspace-engine/pkg/workspace/store" +) + +type getterPostgres struct { + approvalGetters + environmentprogressionGetters + deploymentwindowGetters + deploymentdependencyGetters + versioncooldownGetters + deployableversionsGetters + gradualrolloutGetters +} + +func NewGetterPostgres(store *store.Store) Getter { + return &getterPostgres{} +} \ No newline at end of file diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval.go b/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval.go index 1e61c9428..d2fadaf5c 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval.go @@ -13,28 +13,17 @@ import ( "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentdependency" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentwindow" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versioncooldown" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector" ) -// Getter provides the data-access methods needed by policy evaluators. -type Getter interface { - approval.Getters - environmentprogression.Getters - deploymentwindow.Getters - deploymentdependency.Getters - versioncooldown.Getters - deployableversions.Getters - - GetPolicySkips(ctx context.Context, versionID, environmentID, resourceID string) ([]*oapi.PolicySkip, error) -} - // ruleEvaluators returns evaluators for a single policy rule. func ruleEvaluators(ctx context.Context, getter Getter, rule *oapi.PolicyRule) []evaluator.Evaluator { return evaluator.CollectEvaluators( approval.NewEvaluator(getter, rule), environmentprogression.NewEvaluator(getter, rule), - // gradualrollout.NewEvaluator(getter, rule), + gradualrollout.NewEvaluator(getter, rule), versionselector.NewEvaluator(rule), deploymentdependency.NewEvaluator(getter, rule), deploymentwindow.NewEvaluator(getter, rule),