Skip to content

Commit

Permalink
feat: Add final-deletion-state Flag to Set Custom State When Deletion…
Browse files Browse the repository at this point in the history
…Timestamp is Set (#148)

<!-- Thank you for your contribution. Before you submit the pull
request:
1. Follow contributing guidelines, templates, the recommended Git
workflow, and any related documentation.
2. Read and submit the required Contributor Licence Agreements
(https://github.com/kyma-project/community/blob/main/CONTRIBUTING.md#agreements-and-licenses).
3. Test your changes and attach their results to the pull request.
4. Update the relevant documentation.

If the pull request requires a decision, follow the [decision-making
process](https://github.com/kyma-project/community/blob/main/governance.md)
and replace the PR template with the [decision record
template](https://github.com/kyma-project/community/blob/main/.github/ISSUE_TEMPLATE/decision-record.md).
-->

**Description**

Changes proposed in this pull request:

- Added new flag `final-deletion-state` to Template-Operator Manager -
can be used to set a custom state when deletion timestamp is set
(default Deleting)
- Added default state as a patch in kustomization.yaml

**Related issue(s)**
<!-- If you refer to a particular issue, provide its number. For
example, `Resolves #123`, `Fixes #43`, or `See also #33`. -->
Closes #147
  • Loading branch information
LeelaChacha authored Feb 5, 2024
1 parent 72a2d3b commit 21d8e24
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 64 deletions.
6 changes: 6 additions & 0 deletions config/default/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ patches:
value: --final-state=Ready
target:
kind: Deployment
- patch: |-
- op: add
path: /spec/template/spec/containers/0/args/-
value: --final-deletion-state=Deleting
target:
kind: Deployment
components:
- ../crd
Expand Down
32 changes: 26 additions & 6 deletions controllers/sample_controller_rendered_resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ type SampleReconciler struct {
*rest.Config
// EventRecorder for creating k8s events
record.EventRecorder
FinalState v1alpha1.State
FinalState v1alpha1.State
FinalDeletionState v1alpha1.State
}

type ManifestResources struct {
Expand Down Expand Up @@ -112,11 +113,9 @@ func (r *SampleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
// check if deletionTimestamp is set, retry until it gets deleted
status := getStatusFromSample(&objectInstance)

// set state to Deleting if not set for an object with deletion timestamp
if !objectInstance.GetDeletionTimestamp().IsZero() &&
status.State != v1alpha1.StateDeleting {
// if the status is not yet set to deleting, also update the status
return ctrl.Result{}, r.setStatusForObjectInstance(ctx, &objectInstance, status.WithState(v1alpha1.StateDeleting))
// set state to FinalDeletionState (default is Deleting) if not set for an object with deletion timestamp
if !objectInstance.GetDeletionTimestamp().IsZero() && status.State != r.FinalDeletionState {
return ctrl.Result{}, r.setStatusForObjectInstance(ctx, &objectInstance, status.WithState(r.FinalDeletionState))
}

// add finalizer if not present
Expand Down Expand Up @@ -154,6 +153,11 @@ func (r *SampleReconciler) HandleInitialState(ctx context.Context, objectInstanc
func (r *SampleReconciler) HandleProcessingState(ctx context.Context, objectInstance *v1alpha1.Sample) error {
status := getStatusFromSample(objectInstance)
if err := r.processResources(ctx, objectInstance); err != nil {
// stay in Processing state if FinalDeletionState is set to Processing
if !objectInstance.GetDeletionTimestamp().IsZero() && r.FinalDeletionState == v1alpha1.StateProcessing {
return nil
}

r.Event(objectInstance, "Warning", "ResourcesInstall", err.Error())
return r.setStatusForObjectInstance(ctx, objectInstance, status.
WithState(v1alpha1.StateError).
Expand All @@ -171,6 +175,11 @@ func (r *SampleReconciler) HandleErrorState(ctx context.Context, objectInstance
if err := r.processResources(ctx, objectInstance); err != nil {
return err
}

// stay in Error state if FinalDeletionState is set to Error
if !objectInstance.GetDeletionTimestamp().IsZero() && r.FinalDeletionState == v1alpha1.StateError {
return nil
}
// set eventual state to Ready - if no errors were found
return r.setStatusForObjectInstance(ctx, objectInstance, status.
WithState(r.FinalState).
Expand All @@ -196,6 +205,11 @@ func (r *SampleReconciler) HandleDeletingState(ctx context.Context, objectInstan
// so please make sure the types are available on the target cluster
for _, obj := range resourceObjs.Items {
if err = r.Client.Delete(ctx, obj); err != nil && !errors2.IsNotFound(err) {
// stay in Deleting state if FinalDeletionState is set to Deleting
if !objectInstance.GetDeletionTimestamp().IsZero() && r.FinalDeletionState == v1alpha1.StateDeleting {
return nil
}

logger.Error(err, "error during uninstallation of resources")
r.Event(objectInstance, "Warning", "ResourcesDelete", "deleting resources error")
return r.setStatusForObjectInstance(ctx, objectInstance, status.
Expand All @@ -215,6 +229,12 @@ func (r *SampleReconciler) HandleDeletingState(ctx context.Context, objectInstan
func (r *SampleReconciler) HandleReadyState(ctx context.Context, objectInstance *v1alpha1.Sample) error {
status := getStatusFromSample(objectInstance)
if err := r.processResources(ctx, objectInstance); err != nil {
// stay in Ready/Warning state if FinalDeletionState is set to Ready/Warning
if !objectInstance.GetDeletionTimestamp().IsZero() &&
(r.FinalDeletionState == v1alpha1.StateReady || r.FinalDeletionState == v1alpha1.StateWarning) {
return nil
}

r.Event(objectInstance, "Warning", "ResourcesInstall", err.Error())
return r.setStatusForObjectInstance(ctx, objectInstance, status.
WithState(v1alpha1.StateError).
Expand Down
109 changes: 59 additions & 50 deletions controllers/sample_controller_rendered_resources_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package controllers_test

import (
"time"

v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/client-go/kubernetes"
"time"

"github.com/kyma-project/template-operator/api/v1alpha1"

Expand All @@ -17,41 +16,69 @@ import (
)

var (
sampleName = "kyma-sample"
podNs = "redis"
podName = "busybox-pod"
podNs = "redis"
podName = "busybox-pod"
)

func testFn(sampleCR *v1alpha1.Sample, desiredState v1alpha1.State, desiredConditionStatus metav1.ConditionStatus,
resourceCheck func(g Gomega) bool,
) {
// create SampleCR
Expect(k8sClient.Create(ctx, sampleCR)).To(Succeed())
var _ = Describe("Sample CR is created with the correct resource path", Ordered, func() {
sampleCR := createSampleCR("valid-sample", "./test/busybox/manifest")
sampleCRKey := client.ObjectKeyFromObject(sampleCR)

It("should create SampleCR and resources", func() {
Expect(k8sClient.Create(ctx, sampleCR)).To(Succeed())

Eventually(getCRStatus(sampleCRKey)).
WithTimeout(30 * time.Second).
WithPolling(500 * time.Millisecond).
Should(Equal(CRStatus{State: v1alpha1.StateReady, InstallConditionStatus: metav1.ConditionTrue, Err: nil}))

Eventually(getPod(podNs, podName)).
WithTimeout(30 * time.Second).
WithPolling(500 * time.Millisecond).
Should(BeTrue())
})

It("should set state to Warning when deleted after setting FinalDeletionState", func() {
reconciler.FinalDeletionState = v1alpha1.StateWarning
Expect(k8sClient.Delete(ctx, sampleCR)).To(Succeed())

Eventually(getCRStatus(sampleCRKey)).
WithTimeout(30 * time.Second).
WithPolling(500 * time.Millisecond).
Should(Equal(CRStatus{State: v1alpha1.StateWarning,
InstallConditionStatus: metav1.ConditionTrue, Err: nil}))
Consistently(getCRStatus(sampleCRKey)).
WithTimeout(5 * time.Second).
WithPolling(100 * time.Millisecond).
Should(Equal(CRStatus{State: v1alpha1.StateWarning,
InstallConditionStatus: metav1.ConditionTrue, Err: nil}))
})

It("should delete when FinalDeletionState set to Deleting", func() {
reconciler.FinalDeletionState = v1alpha1.StateDeleting
Eventually(checkDeleted(sampleCRKey)).
WithTimeout(30 * time.Second).
WithPolling(500 * time.Millisecond).
Should(BeTrue())
})
})

// check if SampleCR is in the desired State
var _ = Describe("Sample CR is created with an incorrect resource path", Ordered, func() {
sampleCR := createSampleCR("invalid-sample", "./invalid/path")
sampleCRKey := client.ObjectKeyFromObject(sampleCR)
Eventually(getCRStatus(sampleCRKey)).
WithTimeout(30 * time.Second).
WithPolling(500 * time.Millisecond).
Should(Equal(CRStatus{State: desiredState, InstallConditionStatus: desiredConditionStatus, Err: nil}))

// check if deployed resources are up and running
Eventually(resourceCheck).
WithTimeout(30 * time.Second).
WithPolling(500 * time.Millisecond).
Should(BeTrue())

// clean up SampleCR
Expect(k8sClient.Delete(ctx, sampleCR)).To(Succeed())

// check installed resources are deleted
Eventually(checkDeleted(sampleCRKey)).
WithTimeout(30 * time.Second).
WithPolling(500 * time.Millisecond).
Should(BeTrue())
}

func createSampleCR(sampleName string, path string) *v1alpha1.Sample {
It("should create SampleCR in Error state", func() {
Expect(k8sClient.Create(ctx, sampleCR)).To(Succeed())
Eventually(getCRStatus(sampleCRKey)).
WithTimeout(30 * time.Second).
WithPolling(500 * time.Millisecond).
Should(Equal(CRStatus{State: v1alpha1.StateError, InstallConditionStatus: metav1.ConditionFalse, Err: nil}))

Expect(k8sClient.Delete(ctx, sampleCR)).To(Succeed())
})
})

func createSampleCR(sampleName, path string) *v1alpha1.Sample {
return &v1alpha1.Sample{
TypeMeta: metav1.TypeMeta{
Kind: string(v1alpha1.SampleKind),
Expand Down Expand Up @@ -128,21 +155,3 @@ func checkDeleted(sampleObjKey client.ObjectKey) func(g Gomega) bool {
return false
}
}

var _ = Describe("Sample CR scenarios", Ordered, func() {
DescribeTable("should set SampleCR to appropriate states",
testFn,
Entry("when Sample CR is created with the correct resource path",
createSampleCR(sampleName, "./test/busybox/manifest"),
v1alpha1.StateReady,
metav1.ConditionTrue,
getPod(podNs, podName),
),
Entry("when Sample CR is created with an incorrect resource path",
createSampleCR(sampleName, "invalid/path"),
v1alpha1.StateError,
metav1.ConditionFalse,
func(g Gomega) bool { return true },
),
)
})
9 changes: 5 additions & 4 deletions controllers/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,11 @@ var _ = BeforeSuite(func() {
Expect(err).ToNot(HaveOccurred())

reconciler = &controllers.SampleReconciler{
Client: k8sManager.GetClient(),
Scheme: scheme.Scheme,
EventRecorder: k8sManager.GetEventRecorderFor("tests"),
FinalState: operatorkymaprojectiov1alpha1.StateReady,
Client: k8sManager.GetClient(),
Scheme: scheme.Scheme,
EventRecorder: k8sManager.GetEventRecorderFor("tests"),
FinalState: operatorkymaprojectiov1alpha1.StateReady,
FinalDeletionState: operatorkymaprojectiov1alpha1.StateDeleting,
}

err = reconciler.SetupWithManager(k8sManager, rateLimiter)
Expand Down
12 changes: 8 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type FlagVar struct {
rateLimiterFrequency int
rateLimiterBurst int
finalState string
finalDeletionState string
printVersion bool
}

Expand Down Expand Up @@ -116,10 +117,11 @@ func main() {
}

if err = (&controllers.SampleReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
EventRecorder: mgr.GetEventRecorderFor(operatorName),
FinalState: v1alpha1.State(flagVar.finalState),
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
EventRecorder: mgr.GetEventRecorderFor(operatorName),
FinalState: v1alpha1.State(flagVar.finalState),
FinalDeletionState: v1alpha1.State(flagVar.finalDeletionState),
}).SetupWithManager(mgr, rateLimiter); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Sample")
os.Exit(1)
Expand Down Expand Up @@ -159,6 +161,8 @@ func defineFlagVar() *FlagVar {
"Indicates the failure max delay in seconds")
flag.StringVar(&flagVar.finalState, "final-state", string(v1alpha1.StateReady),
"Customize final state, to mimic state behaviour like Ready, Warning")
flag.StringVar(&flagVar.finalDeletionState, "final-deletion-state", string(v1alpha1.StateDeleting),
"Customize final state when module marked for deletion, to mimic state behaviour like Ready, Warning")
flag.BoolVar(&flagVar.printVersion, "version", false, "Prints the operator version and exits")
return flagVar
}

0 comments on commit 21d8e24

Please sign in to comment.