Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,11 @@ func NewEvaluator(store *store.Store, policyRule *oapi.PolicyRule) evaluator.Eva
})
}

// ScopeFields returns ReleaseTarget since deployment window evaluation needs to
// know if the target has a deployed version already.
// ScopeFields returns Version + ReleaseTarget since deployment window evaluation
// needs to know whether this specific version has already been deployed to this
// target.
func (e *DeploymentWindowEvaluator) ScopeFields() evaluator.ScopeFields {
return evaluator.ScopeReleaseTarget
return evaluator.ScopeVersion | evaluator.ScopeReleaseTarget
}

// RuleType returns the rule type identifier for bypass matching.
Expand Down Expand Up @@ -106,6 +107,34 @@ func formatDuration(d time.Duration) string {
return fmt.Sprintf("%dh %dm", hours, minutes)
}

func (e *DeploymentWindowEvaluator) hasPreviouslyDeployedVersion(
releaseTarget *oapi.ReleaseTarget,
versionId string,
) bool {
if releaseTarget == nil || versionId == "" {
return false
}

jobs := e.store.Jobs.GetJobsForReleaseTarget(releaseTarget)
for _, job := range jobs {
if job.Status != oapi.JobStatusSuccessful || job.CompletedAt == nil {
continue
}

release, ok := e.store.Releases.Get(job.ReleaseId)
if !ok || release == nil || release.Version.Id != versionId {
continue
}

verificationStatus := e.store.JobVerifications.GetJobVerificationStatus(job.Id)
if verificationStatus == "" || verificationStatus == oapi.JobVerificationStatusPassed {
return true
}
}

return false
}

// Evaluate checks if the current time is within a deployment window.
func (e *DeploymentWindowEvaluator) Evaluate(
ctx context.Context,
Expand All @@ -114,7 +143,17 @@ func (e *DeploymentWindowEvaluator) Evaluate(
_, span := tracer.Start(ctx, "DeploymentWindowEvaluator.Evaluate")
defer span.End()

_, _, err := e.store.ReleaseTargets.GetCurrentRelease(ctx, scope.ReleaseTarget())
releaseTarget := scope.ReleaseTarget()
candidateVersion := scope.Version
if candidateVersion != nil && e.hasPreviouslyDeployedVersion(releaseTarget, candidateVersion.Id) {
return results.NewAllowedResult("Version was previously deployed to this release target - deployment window ignored").
WithDetail("reason", "version_previously_deployed").
WithDetail("version_id", candidateVersion.Id).
WithDetail("version_tag", candidateVersion.Tag).
WithDetail("release_target", releaseTarget.Key())
}

_, _, err := e.store.ReleaseTargets.GetCurrentRelease(ctx, releaseTarget)
if err != nil {
return results.NewAllowedResult("No previous version deployed - deployment window ignored").
WithDetail("reason", "first_deployment")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func seedSuccessfulRelease(
ctx context.Context,
st *store.Store,
releaseTarget *oapi.ReleaseTarget,
) {
) *oapi.DeploymentVersion {
t.Helper()

versionCreatedAt := time.Now().Add(-2 * time.Hour)
Expand Down Expand Up @@ -99,16 +99,53 @@ func seedSuccessfulRelease(
CreatedAt: completedAt,
}
st.Jobs.Upsert(ctx, job)

return version
}

func createCandidateVersion(
ctx context.Context,
st *store.Store,
deploymentId string,
tag string,
) *oapi.DeploymentVersion {
version := &oapi.DeploymentVersion{
Id: uuid.New().String(),
DeploymentId: deploymentId,
Tag: tag,
CreatedAt: time.Now(),
}
st.DeploymentVersions.Upsert(ctx, version.Id, version)
return version
}

func setupScopeWithDeployedTarget(t *testing.T, st *store.Store) (context.Context, evaluator.EvaluatorScope) {
t.Helper()

ctx, releaseTarget := setupReleaseTarget(t, st)
seedSuccessfulRelease(t, ctx, st, releaseTarget)
candidateVersion := createCandidateVersion(ctx, st, releaseTarget.DeploymentId, "v2.0.0")

return ctx, evaluator.EvaluatorScope{
Environment: &oapi.Environment{Id: releaseTarget.EnvironmentId},
Version: candidateVersion,
Resource: &oapi.Resource{Id: releaseTarget.ResourceId},
Deployment: &oapi.Deployment{Id: releaseTarget.DeploymentId},
}
}

func setupScopeWithPreviouslyDeployedVersion(
t *testing.T,
st *store.Store,
) (context.Context, evaluator.EvaluatorScope) {
t.Helper()

ctx, releaseTarget := setupReleaseTarget(t, st)
deployedVersion := seedSuccessfulRelease(t, ctx, st, releaseTarget)

return ctx, evaluator.EvaluatorScope{
Environment: &oapi.Environment{Id: releaseTarget.EnvironmentId},
Version: deployedVersion,
Resource: &oapi.Resource{Id: releaseTarget.ResourceId},
Deployment: &oapi.Deployment{Id: releaseTarget.DeploymentId},
}
Expand Down Expand Up @@ -166,8 +203,9 @@ func TestDeploymentWindowEvaluator_ScopeFields(t *testing.T) {
eval := NewEvaluator(st, rule)
require.NotNil(t, eval, "expected non-nil evaluator")

// Deployment window needs release target to check for existing deployments
assert.Equal(t, evaluator.ScopeReleaseTarget, eval.ScopeFields())
// Deployment window needs release target + version to evaluate whether this
// candidate version has already been deployed for the target.
assert.Equal(t, evaluator.ScopeVersion|evaluator.ScopeReleaseTarget, eval.ScopeFields())
}

func TestDeploymentWindowEvaluator_RuleType(t *testing.T) {
Expand Down Expand Up @@ -323,8 +361,10 @@ func TestDeploymentWindowEvaluator_IgnoresWindowWithoutDeployedVersion(t *testin
require.NotNil(t, eval, "expected non-nil evaluator")

ctx, releaseTarget := setupReleaseTarget(t, st)
candidateVersion := createCandidateVersion(ctx, st, releaseTarget.DeploymentId, "v1.0.0")
scope := evaluator.EvaluatorScope{
Environment: &oapi.Environment{Id: releaseTarget.EnvironmentId},
Version: candidateVersion,
Resource: &oapi.Resource{Id: releaseTarget.ResourceId},
Deployment: &oapi.Deployment{Id: releaseTarget.DeploymentId},
}
Expand All @@ -335,6 +375,31 @@ func TestDeploymentWindowEvaluator_IgnoresWindowWithoutDeployedVersion(t *testin
assert.Equal(t, "first_deployment", result.Details["reason"])
}

func TestDeploymentWindowEvaluator_IgnoresWindowForPreviouslyDeployedVersion(t *testing.T) {
st := setupStore()

// Outside this allow window by default, so this would normally be blocked.
rule := &oapi.PolicyRule{
Id: "rule-1",
DeploymentWindow: &oapi.DeploymentWindowRule{
Rrule: "FREQ=YEARLY;COUNT=1;DTSTART=20200101T000000Z",
DurationMinutes: 1,
AllowWindow: boolPtr(true),
},
}

eval := NewEvaluator(st, rule)
require.NotNil(t, eval, "expected non-nil evaluator")

ctx, scope := setupScopeWithPreviouslyDeployedVersion(t, st)
result := eval.Evaluate(ctx, scope)

assert.True(t, result.Allowed, "expected allowed when candidate version was previously deployed")
assert.Contains(t, result.Message, "deployment window ignored")
assert.Equal(t, "version_previously_deployed", result.Details["reason"])
assert.Equal(t, scope.Version.Id, result.Details["version_id"])
}

func TestDeploymentWindowEvaluator_NextEvaluationTime(t *testing.T) {
st := setupStore()

Expand Down
Loading