From a04230bf9d65bb68a742feef0a7d4853abe32b12 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Mon, 11 May 2026 10:34:20 -0400 Subject: [PATCH 1/3] fix: if candidate version is already rolled out gradual rollout evaluator short circuits --- .../evaluator/gradualrollout/getters.go | 59 ++++++++++--- .../gradualrollout/gradualrollout.go | 13 +++ .../gradualrollout/gradualrollout_test.go | 84 +++++++++++++++++-- .../policyeval/policyeval_test.go | 6 ++ .../desiredrelease/reconcile_test.go | 7 ++ .../test/controllers/harness/mocks.go | 15 ++++ 6 files changed, 167 insertions(+), 17 deletions(-) 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 015613781..054d25283 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 @@ -2,9 +2,11 @@ package gradualrollout import ( "context" + "errors" "fmt" "github.com/google/uuid" + "github.com/jackc/pgx/v5" "workspace-engine/pkg/db" "workspace-engine/pkg/oapi" "workspace-engine/pkg/store/policies" @@ -13,6 +15,22 @@ import ( "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression" ) +func parseReleaseTargetUUIDs(rt *oapi.ReleaseTarget) (uuid.UUID, uuid.UUID, uuid.UUID, error) { + resourceID, err := uuid.Parse(rt.ResourceId) + if err != nil { + return uuid.Nil, uuid.Nil, uuid.Nil, fmt.Errorf("parse resource id: %w", err) + } + environmentID, err := uuid.Parse(rt.EnvironmentId) + if err != nil { + return uuid.Nil, uuid.Nil, uuid.Nil, fmt.Errorf("parse environment id: %w", err) + } + deploymentID, err := uuid.Parse(rt.DeploymentId) + if err != nil { + return uuid.Nil, uuid.Nil, uuid.Nil, fmt.Errorf("parse deployment id: %w", err) + } + return resourceID, environmentID, deploymentID, nil +} + type approvalGetters = approval.Getters type environmentProgressionGetters = environmentprogression.Getters @@ -29,6 +47,7 @@ type Getters interface { versionID, environmentID, resourceID string, ) ([]*oapi.PolicySkip, error) HasCurrentRelease(ctx context.Context, releaseTarget *oapi.ReleaseTarget) (bool, error) + GetCurrentVersionID(ctx context.Context, releaseTarget *oapi.ReleaseTarget) (*string, error) } // --------------------------------------------------------------------------- @@ -99,28 +118,46 @@ func (p *PostgresGetters) GetPolicySkips( return ps, nil } -func (p *PostgresGetters) HasCurrentRelease( +func (p *PostgresGetters) GetCurrentVersionID( ctx context.Context, releaseTarget *oapi.ReleaseTarget, -) (bool, error) { - resourceIDUUID, err := uuid.Parse(releaseTarget.ResourceId) +) (*string, error) { + resourceID, environmentID, deploymentID, err := parseReleaseTargetUUIDs(releaseTarget) if err != nil { - return false, fmt.Errorf("parse resource id: %w", err) + return nil, err } - environmentIDUUID, err := uuid.Parse(releaseTarget.EnvironmentId) + row, err := p.queries.GetCurrentReleaseByReleaseTarget( + ctx, + db.GetCurrentReleaseByReleaseTargetParams{ + ResourceID: resourceID, + EnvironmentID: environmentID, + DeploymentID: deploymentID, + }, + ) if err != nil { - return false, fmt.Errorf("parse environment id: %w", err) + if errors.Is(err, pgx.ErrNoRows) { + return nil, nil + } + return nil, err } - deploymentIDUUID, err := uuid.Parse(releaseTarget.DeploymentId) + s := row.VersionID.String() + return &s, nil +} + +func (p *PostgresGetters) HasCurrentRelease( + ctx context.Context, + releaseTarget *oapi.ReleaseTarget, +) (bool, error) { + resourceID, environmentID, deploymentID, err := parseReleaseTargetUUIDs(releaseTarget) if err != nil { - return false, fmt.Errorf("parse deployment id: %w", err) + return false, err } releases, err := p.queries.ListReleasesByReleaseTarget( ctx, db.ListReleasesByReleaseTargetParams{ - ResourceID: resourceIDUUID, - EnvironmentID: environmentIDUUID, - DeploymentID: deploymentIDUUID, + ResourceID: resourceID, + EnvironmentID: environmentID, + DeploymentID: deploymentID, }, ) if err != nil { 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 ff9b9dd01..7133cb08f 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 @@ -357,6 +357,19 @@ func (e *GradualRolloutEvaluator) Evaluate( WithDetail("error", err.Error()) } + currentVersionID, err := e.getters.GetCurrentVersionID(ctx, releaseTarget) + if err != nil { + return results. + NewDeniedResult(fmt.Sprintf("Failed to get current version: %v", err)). + WithDetail("error", err.Error()) + } + if currentVersionID != nil && *currentVersionID == version.Id { + return results. + NewAllowedResult("Resource already on this version; gradual rollout does not revert"). + WithDetail("resource", resource). + WithDetail("current_version_id", version.Id) + } + releaseTargets, err := e.getReleaseTargets(ctx, environment, version) if err != nil { return results. 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 e05133d7c..190ae4984 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 @@ -17,12 +17,13 @@ import ( // mockGetters implements the full Getters interface for testing. type mockGetters struct { - resources map[string]*oapi.Resource - releaseTargets []*oapi.ReleaseTarget - policies []*oapi.Policy - policySkips []*oapi.PolicySkip - hasRelease bool - approvalRecords []*oapi.UserApprovalRecord + resources map[string]*oapi.Resource + releaseTargets []*oapi.ReleaseTarget + policies []*oapi.Policy + policySkips []*oapi.PolicySkip + hasRelease bool + currentVersionID *string + approvalRecords []*oapi.UserApprovalRecord environments map[string]*oapi.Environment deployments map[string]*oapi.Deployment @@ -153,6 +154,13 @@ func (m *mockGetters) HasCurrentRelease(_ context.Context, _ *oapi.ReleaseTarget return m.hasRelease, nil } +func (m *mockGetters) GetCurrentVersionID( + _ context.Context, + _ *oapi.ReleaseTarget, +) (*string, error) { + return m.currentVersionID, nil +} + func (m *mockGetters) GetJobsForEnvironmentAndVersion( _ context.Context, _, _ string, @@ -1436,6 +1444,70 @@ func TestGradualRolloutEvaluator_DeploymentWindow_DenyWindowPreventsFrontloading } } +// --------------------------------------------------------------------------- +// Tests: Already-on-version short-circuit (rollback prevention) +// --------------------------------------------------------------------------- + +// Scenario: a resource advanced to version V while a policy override was active. +// The override has now expired. Without the short-circuit, gradual rollout +// would re-evaluate the curve, find this resource's slot hasn't been reached, +// return Pending, and the desired-release loop would fall back to V-1 — rolling +// the resource back. With the short-circuit, evaluating V against a resource +// already on V returns Allowed regardless of the curve. +func TestGradualRolloutEvaluator_AlreadyOnVersion_AllowsRegardlessOfCurve(t *testing.T) { + baseTime := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) + ts := newTestSetup(5, baseTime) + + // Pretend the resource at position 4 (whose curve slot is at t+240s) is + // already on this version because it advanced during an override. + ts.mock.currentVersionID = &ts.version.Id + + // Current time is 30 seconds in — only position 0 should be "on the curve." + // Position 4 would normally be Pending. + thirtySecLater := baseTime.Add(30 * time.Second) + rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) + eval := ts.eval(rule, func() time.Time { return thirtySecLater }) + + result := eval.Evaluate(ts.ctx, ts.scope(4)) + assert.True( + t, + result.Allowed, + "resource already on this version should be allowed regardless of curve position", + ) +} + +// A resource on a DIFFERENT version (e.g., the prior version) must still be +// gated by the curve — the short-circuit must not leak into the normal case. +func TestGradualRolloutEvaluator_OnDifferentVersion_CurveStillGates(t *testing.T) { + baseTime := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) + ts := newTestSetup(5, baseTime) + + priorVersionID := uuid.New().String() + ts.mock.currentVersionID = &priorVersionID + + thirtySecLater := baseTime.Add(30 * time.Second) + rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) + eval := ts.eval(rule, func() time.Time { return thirtySecLater }) + + result := eval.Evaluate(ts.ctx, ts.scope(4)) + assert.False(t, result.Allowed, "resource not yet on candidate version must wait for its slot") +} + +// No prior release at all: evaluator falls through to normal curve evaluation. +func TestGradualRolloutEvaluator_NoCurrentVersion_CurveStillGates(t *testing.T) { + baseTime := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) + ts := newTestSetup(5, baseTime) + + ts.mock.currentVersionID = nil // never deployed + + thirtySecLater := baseTime.Add(30 * time.Second) + rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) + eval := ts.eval(rule, func() time.Time { return thirtySecLater }) + + result := eval.Evaluate(ts.ctx, ts.scope(4)) + assert.False(t, result.Allowed, "first-time deploys must still respect the curve") +} + // --------------------------------------------------------------------------- // Tests: NewEvaluator constructor // --------------------------------------------------------------------------- diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval_test.go b/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval_test.go index 7193388b5..9a38ac4c0 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval_test.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval_test.go @@ -97,6 +97,12 @@ func (m *mockGetter) GetApprovalRecords( func (m *mockGetter) HasCurrentRelease(_ context.Context, _ *oapi.ReleaseTarget) (bool, error) { return false, nil } +func (m *mockGetter) GetCurrentVersionID( + _ context.Context, + _ *oapi.ReleaseTarget, +) (*string, error) { + return nil, nil +} func (m *mockGetter) GetPolicySkips(_ context.Context, _, _, _ string) ([]*oapi.PolicySkip, error) { return m.policySkips, m.policySkipsErr } diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go b/apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go index fdf463c65..a00c1f460 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go @@ -126,6 +126,13 @@ func (m *mockReconcileGetter) HasCurrentRelease( return false, nil } +func (m *mockReconcileGetter) GetCurrentVersionID( + _ context.Context, + _ *oapi.ReleaseTarget, +) (*string, error) { + return nil, nil +} + func (m *mockReconcileGetter) GetEnvironment( _ context.Context, _ string, diff --git a/apps/workspace-engine/test/controllers/harness/mocks.go b/apps/workspace-engine/test/controllers/harness/mocks.go index 190c3d654..c7d00b5f1 100644 --- a/apps/workspace-engine/test/controllers/harness/mocks.go +++ b/apps/workspace-engine/test/controllers/harness/mocks.go @@ -151,6 +151,11 @@ type DesiredReleaseGetter struct { // HasCurrentReleaseFn allows per-release-target logic when set. HasCurrentReleaseFn func(rt *oapi.ReleaseTarget) bool + + CurrentVersionID *string + + // CurrentVersionIDFn allows per-release-target logic when set. + CurrentVersionIDFn func(rt *oapi.ReleaseTarget) *string } func (g *DesiredReleaseGetter) ReleaseTargetExists( @@ -209,6 +214,16 @@ func (g *DesiredReleaseGetter) HasCurrentRelease( return g.HasRelease, nil } +func (g *DesiredReleaseGetter) GetCurrentVersionID( + _ context.Context, + rt *oapi.ReleaseTarget, +) (*string, error) { + if g.CurrentVersionIDFn != nil { + return g.CurrentVersionIDFn(rt), nil + } + return g.CurrentVersionID, nil +} + func (g *DesiredReleaseGetter) GetCurrentRelease( _ context.Context, _ *desiredrelease.ReleaseTarget, From 1238d72ce7d6f6f7d4e2a1c80d4412c4e2230779 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Mon, 11 May 2026 10:58:17 -0400 Subject: [PATCH 2/3] comments --- .../pkg/db/queries/releases.sql | 16 ++++++++++ apps/workspace-engine/pkg/db/releases.sql.go | 30 +++++++++++++++++++ .../evaluator/gradualrollout/getters.go | 6 ++-- .../gradualrollout/gradualrollout_test.go | 10 ++++--- 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/apps/workspace-engine/pkg/db/queries/releases.sql b/apps/workspace-engine/pkg/db/queries/releases.sql index 33cb87e68..26167529a 100644 --- a/apps/workspace-engine/pkg/db/queries/releases.sql +++ b/apps/workspace-engine/pkg/db/queries/releases.sql @@ -106,6 +106,22 @@ LEFT JOIN release_variable rv ON rv.release_id = r.id WHERE rtr.resource_id = $1 AND rtr.environment_id = $2 AND rtr.deployment_id = $3 GROUP BY r.id, dv.id; +-- name: GetCurrentVersionIDByReleaseTarget :one +-- Returns the version_id of the latest successful release for a release target. +-- Lightweight variant of GetCurrentReleaseByReleaseTarget for callers that +-- only need the version identifier (e.g., gradual rollout short-circuit). +SELECT r.version_id +FROM release r +JOIN release_job rj ON rj.release_id = r.id +JOIN job j ON j.id = rj.job_id +WHERE r.resource_id = @resource_id + AND r.environment_id = @environment_id + AND r.deployment_id = @deployment_id + AND j.status = 'successful' + AND j.completed_at IS NOT NULL +ORDER BY j.completed_at DESC +LIMIT 1; + -- name: GetCurrentReleaseByReleaseTarget :one -- Returns the release associated with the latest successful job for a release target, -- including version and variables. diff --git a/apps/workspace-engine/pkg/db/releases.sql.go b/apps/workspace-engine/pkg/db/releases.sql.go index 24b8e8441..8e02d5733 100644 --- a/apps/workspace-engine/pkg/db/releases.sql.go +++ b/apps/workspace-engine/pkg/db/releases.sql.go @@ -183,6 +183,36 @@ func (q *Queries) GetCurrentReleaseByReleaseTarget(ctx context.Context, arg GetC return i, err } +const getCurrentVersionIDByReleaseTarget = `-- name: GetCurrentVersionIDByReleaseTarget :one +SELECT r.version_id +FROM release r +JOIN release_job rj ON rj.release_id = r.id +JOIN job j ON j.id = rj.job_id +WHERE r.resource_id = $1 + AND r.environment_id = $2 + AND r.deployment_id = $3 + AND j.status = 'successful' + AND j.completed_at IS NOT NULL +ORDER BY j.completed_at DESC +LIMIT 1 +` + +type GetCurrentVersionIDByReleaseTargetParams struct { + ResourceID uuid.UUID + EnvironmentID uuid.UUID + DeploymentID uuid.UUID +} + +// Returns the version_id of the latest successful release for a release target. +// Lightweight variant of GetCurrentReleaseByReleaseTarget for callers that +// only need the version identifier (e.g., gradual rollout short-circuit). +func (q *Queries) GetCurrentVersionIDByReleaseTarget(ctx context.Context, arg GetCurrentVersionIDByReleaseTargetParams) (uuid.UUID, error) { + row := q.db.QueryRow(ctx, getCurrentVersionIDByReleaseTarget, arg.ResourceID, arg.EnvironmentID, arg.DeploymentID) + var version_id uuid.UUID + err := row.Scan(&version_id) + return version_id, err +} + const getDesiredReleaseByReleaseTarget = `-- name: GetDesiredReleaseByReleaseTarget :one SELECT r.id, r.resource_id, r.environment_id, r.deployment_id, r.version_id, r.created_at, 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 054d25283..98b3852b8 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 @@ -126,9 +126,9 @@ func (p *PostgresGetters) GetCurrentVersionID( if err != nil { return nil, err } - row, err := p.queries.GetCurrentReleaseByReleaseTarget( + versionID, err := p.queries.GetCurrentVersionIDByReleaseTarget( ctx, - db.GetCurrentReleaseByReleaseTargetParams{ + db.GetCurrentVersionIDByReleaseTargetParams{ ResourceID: resourceID, EnvironmentID: environmentID, DeploymentID: deploymentID, @@ -140,7 +140,7 @@ func (p *PostgresGetters) GetCurrentVersionID( } return nil, err } - s := row.VersionID.String() + s := versionID.String() return &s, nil } 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 190ae4984..1438ef968 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 @@ -1458,12 +1458,14 @@ func TestGradualRolloutEvaluator_AlreadyOnVersion_AllowsRegardlessOfCurve(t *tes baseTime := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) ts := newTestSetup(5, baseTime) - // Pretend the resource at position 4 (whose curve slot is at t+240s) is - // already on this version because it advanced during an override. + // Mock all resources as already on this version (simulates the override- + // induced advancement). The mock is global per-test, so every release target + // reports the same current version — that's fine; we only assert on the one + // resource whose curve slot the test actually cares about. ts.mock.currentVersionID = &ts.version.Id - // Current time is 30 seconds in — only position 0 should be "on the curve." - // Position 4 would normally be Pending. + // Current time is 30 seconds in — without the short-circuit, position 4's + // curve slot is at t+240s and would return Pending. thirtySecLater := baseTime.Add(30 * time.Second) rule := createGradualRolloutRule(oapi.GradualRolloutRuleRolloutTypeLinear, 60) eval := ts.eval(rule, func() time.Time { return thirtySecLater }) From 4638db10b95839caa54af3cecd748fb89d3c45da Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Mon, 11 May 2026 11:12:05 -0400 Subject: [PATCH 3/3] nit --- .../evaluator/gradualrollout/getters.go | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) 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 98b3852b8..40e68d160 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 @@ -15,20 +15,30 @@ import ( "workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression" ) -func parseReleaseTargetUUIDs(rt *oapi.ReleaseTarget) (uuid.UUID, uuid.UUID, uuid.UUID, error) { +type ReleaseTargetIDs struct { + EnvironmentID uuid.UUID + ResourceID uuid.UUID + DeploymentID uuid.UUID +} + +func parseReleaseTargetUUIDs(rt *oapi.ReleaseTarget) (ReleaseTargetIDs, error) { resourceID, err := uuid.Parse(rt.ResourceId) if err != nil { - return uuid.Nil, uuid.Nil, uuid.Nil, fmt.Errorf("parse resource id: %w", err) + return ReleaseTargetIDs{}, fmt.Errorf("parse resource id: %w", err) } environmentID, err := uuid.Parse(rt.EnvironmentId) if err != nil { - return uuid.Nil, uuid.Nil, uuid.Nil, fmt.Errorf("parse environment id: %w", err) + return ReleaseTargetIDs{}, fmt.Errorf("parse environment id: %w", err) } deploymentID, err := uuid.Parse(rt.DeploymentId) if err != nil { - return uuid.Nil, uuid.Nil, uuid.Nil, fmt.Errorf("parse deployment id: %w", err) + return ReleaseTargetIDs{}, fmt.Errorf("parse deployment id: %w", err) } - return resourceID, environmentID, deploymentID, nil + return ReleaseTargetIDs{ + EnvironmentID: environmentID, + ResourceID: resourceID, + DeploymentID: deploymentID, + }, nil } type approvalGetters = approval.Getters @@ -122,16 +132,16 @@ func (p *PostgresGetters) GetCurrentVersionID( ctx context.Context, releaseTarget *oapi.ReleaseTarget, ) (*string, error) { - resourceID, environmentID, deploymentID, err := parseReleaseTargetUUIDs(releaseTarget) + ids, err := parseReleaseTargetUUIDs(releaseTarget) if err != nil { return nil, err } versionID, err := p.queries.GetCurrentVersionIDByReleaseTarget( ctx, db.GetCurrentVersionIDByReleaseTargetParams{ - ResourceID: resourceID, - EnvironmentID: environmentID, - DeploymentID: deploymentID, + ResourceID: ids.ResourceID, + EnvironmentID: ids.EnvironmentID, + DeploymentID: ids.DeploymentID, }, ) if err != nil { @@ -148,16 +158,16 @@ func (p *PostgresGetters) HasCurrentRelease( ctx context.Context, releaseTarget *oapi.ReleaseTarget, ) (bool, error) { - resourceID, environmentID, deploymentID, err := parseReleaseTargetUUIDs(releaseTarget) + ids, err := parseReleaseTargetUUIDs(releaseTarget) if err != nil { return false, err } releases, err := p.queries.ListReleasesByReleaseTarget( ctx, db.ListReleasesByReleaseTargetParams{ - ResourceID: resourceID, - EnvironmentID: environmentID, - DeploymentID: deploymentID, + ResourceID: ids.ResourceID, + EnvironmentID: ids.EnvironmentID, + DeploymentID: ids.DeploymentID, }, ) if err != nil {