diff --git a/api/v1alpha1/common.go b/api/v1alpha1/common.go index e9efe1b6..bee80707 100644 --- a/api/v1alpha1/common.go +++ b/api/v1alpha1/common.go @@ -11,9 +11,10 @@ type OverrideRunnerSpec struct { ServiceAccountName string `json:"serviceAccountName,omitempty"` } -type RemediationStrategy struct { - PlanOnDrift bool `json:"planOnDrift,omitempty"` - ApplyOnDrift bool `json:"applyOnDrift,omitempty"` - PlanOnPush bool `json:"planOnPush,omitempty"` - ApplyOnPush bool `json:"applyOnPush,omitempty"` -} +// +kubebuilder:validation:Enum=dry;autoApply +type RemediationStrategy string + +const ( + DryRemediationStrategy RemediationStrategy = "dry" + AutoApplyRemediationStrategy RemediationStrategy = "autoApply" +) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 2690a20a..461020b0 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -22,7 +22,7 @@ limitations under the License. package v1alpha1 import ( - v1 "k8s.io/api/core/v1" + "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -61,21 +61,6 @@ func (in *OverrideRunnerSpec) DeepCopy() *OverrideRunnerSpec { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RemediationStrategy) DeepCopyInto(out *RemediationStrategy) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RemediationStrategy. -func (in *RemediationStrategy) DeepCopy() *RemediationStrategy { - if in == nil { - return nil - } - out := new(RemediationStrategy) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TerraformLayer) DeepCopyInto(out *TerraformLayer) { *out = *in @@ -154,7 +139,6 @@ func (in *TerraformLayerRepository) DeepCopy() *TerraformLayerRepository { func (in *TerraformLayerSpec) DeepCopyInto(out *TerraformLayerSpec) { *out = *in out.Repository = in.Repository - out.RemediationStrategy = in.RemediationStrategy in.OverrideRunnerSpec.DeepCopyInto(&out.OverrideRunnerSpec) } @@ -268,7 +252,6 @@ func (in *TerraformRepositoryRepository) DeepCopy() *TerraformRepositoryReposito func (in *TerraformRepositorySpec) DeepCopyInto(out *TerraformRepositorySpec) { *out = *in out.Repository = in.Repository - out.RemediationStrategy = in.RemediationStrategy in.OverrideRunnerSpec.DeepCopyInto(&out.OverrideRunnerSpec) } diff --git a/config/crd/bases/config.terraform.padok.cloud_terraformlayers.yaml b/config/crd/bases/config.terraform.padok.cloud_terraformlayers.yaml index 9c3ccff1..c1ca8994 100644 --- a/config/crd/bases/config.terraform.padok.cloud_terraformlayers.yaml +++ b/config/crd/bases/config.terraform.padok.cloud_terraformlayers.yaml @@ -103,14 +103,10 @@ spec: planOnPullRequest: type: boolean remediationStrategy: - properties: - applyOnDrift: - type: boolean - applyOnPush: - type: boolean - planOnDrift: - type: boolean - type: object + enum: + - dry + - autoApply + type: string repository: properties: kind: diff --git a/config/crd/bases/config.terraform.padok.cloud_terraformrepositories.yaml b/config/crd/bases/config.terraform.padok.cloud_terraformrepositories.yaml index 96fb389c..676df2e2 100644 --- a/config/crd/bases/config.terraform.padok.cloud_terraformrepositories.yaml +++ b/config/crd/bases/config.terraform.padok.cloud_terraformrepositories.yaml @@ -97,6 +97,11 @@ spec: type: object type: array type: object + remediationStrategy: + enum: + - dry + - autoApply + type: string repository: properties: secretName: diff --git a/internal/annotations/annotations.go b/internal/annotations/annotations.go index a6f52ca8..41213300 100644 --- a/internal/annotations/annotations.go +++ b/internal/annotations/annotations.go @@ -18,8 +18,10 @@ const ( Failure string = "runner.terraform.padok.cloud/failure" Lock string = "runner.terraform.padok.cloud/lock" - LastBranchCommit string = "webhook.terraform.padok.cloud/branch-commit" - ForceApply string = "notifications.terraform.padok.cloud/force-apply" + LastBranchCommit string = "webhook.terraform.padok.cloud/branch-commit" + LastConcerningCommit string = "webhook.terraform.padok.cloud/concerning-commit" + + ForceApply string = "notifications.terraform.padok.cloud/force-apply" ) func Add(ctx context.Context, c client.Client, obj configv1alpha1.TerraformLayer, annotations map[string]string) error { diff --git a/internal/controllers/terraformlayer/conditions.go b/internal/controllers/terraformlayer/conditions.go index 61b279e9..d3a13390 100644 --- a/internal/controllers/terraformlayer/conditions.go +++ b/internal/controllers/terraformlayer/conditions.go @@ -51,9 +51,9 @@ func (r *Reconciler) IsPlanArtifactUpToDate(t *configv1alpha1.TerraformLayer) (m return condition, false } -func (r *Reconciler) IsLastCommitPlanned(t *configv1alpha1.TerraformLayer) (metav1.Condition, bool) { +func (r *Reconciler) IsLastConcernginCommitPlanned(t *configv1alpha1.TerraformLayer) (metav1.Condition, bool) { condition := metav1.Condition{ - Type: "IsLastCommitPlanned", + Type: "IsLastConcerningCommitPlanned", ObservedGeneration: t.GetObjectMeta().GetGeneration(), Status: metav1.ConditionUnknown, LastTransitionTime: metav1.NewTime(time.Now()), @@ -72,16 +72,29 @@ func (r *Reconciler) IsLastCommitPlanned(t *configv1alpha1.TerraformLayer) (meta condition.Status = metav1.ConditionTrue return condition, true } + lastConcerningCommit, ok := t.Annotations[annotations.LastConcerningCommit] + if !ok { + condition.Reason = "NoCommitReceived" + condition.Message = "No commit has been received from webhook" + condition.Status = metav1.ConditionTrue + return condition, true + } + if lastBranchCommit != lastConcerningCommit { + condition.Reason = "CommitAlreadyHadnled" + condition.Message = "The last concerning commit should already have been planned" + condition.Status = metav1.ConditionTrue + return condition, true + } if lastPlannedCommit == lastBranchCommit { - condition.Reason = "LastCommitPlanned" - condition.Message = "The last commit has already been planned" + condition.Reason = "LastConcerningCommitPlanned" + condition.Message = "The last concerngin commit has already been planned" condition.Status = metav1.ConditionTrue - return condition, false + return condition, true } - condition.Reason = "LastCommitNotPlanned" - condition.Message = "The last received commit has not been planned yet" + condition.Reason = "LastConcerningCommitNotPlanned" + condition.Message = "The last received concerning commit has not been planned yet" condition.Status = metav1.ConditionFalse - return condition, true + return condition, false } func (r *Reconciler) IsApplyUpToDate(t *configv1alpha1.TerraformLayer) (metav1.Condition, bool) { diff --git a/internal/controllers/terraformlayer/states.go b/internal/controllers/terraformlayer/states.go index 46a551ee..be4ba29e 100644 --- a/internal/controllers/terraformlayer/states.go +++ b/internal/controllers/terraformlayer/states.go @@ -19,19 +19,19 @@ func (r *Reconciler) GetState(ctx context.Context, l *configv1alpha1.TerraformLa log := log.FromContext(ctx) c1, isPlanArtifactUpToDate := r.IsPlanArtifactUpToDate(l) c2, isApplyUpToDate := r.IsApplyUpToDate(l) - c3, isLastCommitPlanned := r.IsLastCommitPlanned(l) + c3, isLastConcerningCommitPlanned := r.IsLastConcernginCommitPlanned(l) // c3, hasFailed := HasFailed(r) conditions := []metav1.Condition{c1, c2, c3} switch { case isPlanArtifactUpToDate && isApplyUpToDate: log.Info("Layer is up to date, waiting for a new drift detection cycle") return &IdleState{}, conditions - case isPlanArtifactUpToDate && !isApplyUpToDate && !isLastCommitPlanned: - log.Info("Layer needs to be applied, acquiring lock and creating a new runner") - return &ApplyNeededState{}, conditions - case !isPlanArtifactUpToDate || !isLastCommitPlanned: + case !isPlanArtifactUpToDate || !isLastConcerningCommitPlanned: log.Info("Layer needs to be planned, acquiring lock and creating a new runner") return &PlanNeededState{}, conditions + case isPlanArtifactUpToDate && !isApplyUpToDate: + log.Info("Layer needs to be applied, acquiring lock and creating a new runner") + return &ApplyNeededState{}, conditions default: log.Info("Layer is in an unknown state, defaulting to idle. If this happens please file an issue, this is an intended behavior.") return &IdleState{}, conditions @@ -88,6 +88,16 @@ type ApplyNeededState struct{} func (s *ApplyNeededState) getHandler() func(ctx context.Context, t *Reconciler, r *configv1alpha1.TerraformLayer, repository *configv1alpha1.TerraformRepository) ctrl.Result { return func(ctx context.Context, t *Reconciler, r *configv1alpha1.TerraformLayer, repository *configv1alpha1.TerraformRepository) ctrl.Result { log := log.FromContext(ctx) + deltaDriftDetection, err := time.ParseDuration(t.Config.Controller.Timers.DriftDetection) + if err != nil { + log.Error(err, "could not parse timer drift detection period") + return ctrl.Result{} + } + remediationStrategy := getRemediationStrategy(repository, r) + if remediationStrategy != configv1alpha1.AutoApplyRemediationStrategy { + log.Info("layer is in dry mode, no action taken") + return ctrl.Result{RequeueAfter: deltaDriftDetection} + } deltaOnError, err := time.ParseDuration(t.Config.Controller.Timers.OnError) if err != nil { log.Error(err, "could not parse timer on error period") @@ -113,3 +123,14 @@ func (s *ApplyNeededState) getHandler() func(ctx context.Context, t *Reconciler, return ctrl.Result{RequeueAfter: delta} } } + +func getRemediationStrategy(repo *configv1alpha1.TerraformRepository, layer *configv1alpha1.TerraformLayer) configv1alpha1.RemediationStrategy { + result := configv1alpha1.DryRemediationStrategy + if len(repo.Spec.RemediationStrategy) > 0 { + result = repo.Spec.RemediationStrategy + } + if len(layer.Spec.RemediationStrategy) > 0 { + result = layer.Spec.RemediationStrategy + } + return result +} diff --git a/internal/webhook/webhook.go b/internal/webhook/webhook.go index c7cafa39..d6e0dfe7 100644 --- a/internal/webhook/webhook.go +++ b/internal/webhook/webhook.go @@ -131,17 +131,18 @@ func (w *Webhook) Handle(payload interface{}) { log.Println("could not get layers") } for _, layer := range layers.Items { + ann := map[string]string{} if layer.Spec.Branch != revision { continue } + ann[annotations.LastBranchCommit] = change.shaAfter log.Printf("Evaluating %s", layer.Name) if layerFilesHaveChanged(&layer, changedFiles) { - ann := map[string]string{} - ann[annotations.LastBranchCommit] = change.shaAfter - err = annotations.Add(context.TODO(), w.Client, layer, ann) - if err != nil { - log.Printf("Error adding annotation to layer %s", err) - } + ann[annotations.LastConcerningCommit] = change.shaAfter + } + err = annotations.Add(context.TODO(), w.Client, layer, ann) + if err != nil { + log.Printf("Error adding annotation to layer %s", err) } } }