From 214d982c159f73bfc27ad9ac666922190cf654b4 Mon Sep 17 00:00:00 2001 From: achimweigel Date: Fri, 15 Sep 2023 09:31:42 +0200 Subject: [PATCH] Delete NamespaceRegistration with Real Phase (#217) * fix delete namespaceregistration (run-int-tests) * fix delete namespaceregistration (run-int-tests) * fix delete namespaceregistration (run-int-tests) * fix delete namespaceregistration (run-int-tests) * fix delete namespaceregistration (run-int-tests) * real phase and last error for namespaceregistration (run-int-tests) * real phase and last error for namespaceregistration (run-int-tests) * real phase and last error for namespaceregistration (run-int-tests) * fix namespaceregistration (run-int-tests) * fix namespaceregistration (run-int-tests) * fix namespaceregistration (run-int-tests) * Phase constants * Error reasons * fix namespaceregistration (run-int-tests) * Requeue duration * Handle invalid name * Requeuing in unit test * Formatting (run-int-tests) * Deletion strategies * fix namespaceregistration (run-int-tests) * fix namespaceregistration (run-int-tests) * fix namespaceregistration (run-int-tests) * fix namespaceregistration * Constant for deletion strategy * Docu (run-int-tests) * Unit tests (run-int-tests) * Unit tests (run-int-tests) * Unit tests (run-int-tests) --------- Co-authored-by: Robert Graeff --- Makefile | 5 +- docs/architecture/architecture.md | 17 +- docs/usage/Namespaceregistration.md | 87 ++++ hack/setup-testenv.sh | 2 +- .../apis/core/types_namespaceregistration.go | 2 + .../pkg/apis/core/v1alpha1/constants.go | 4 + .../v1alpha1/types_namespaceregistration.go | 2 + .../core/v1alpha1/zz_generated.conversion.go | 2 + .../core/v1alpha1/zz_generated.deepcopy.go | 7 +- .../pkg/apis/core/zz_generated.deepcopy.go | 7 +- .../landscaper-service/pkg/utils/utils.go | 20 + .../core-v1alpha1-NamespaceRegistration.json | 41 ++ pkg/apis/core/types_namespaceregistration.go | 2 + pkg/apis/core/v1alpha1/constants.go | 4 + .../v1alpha1/types_namespaceregistration.go | 2 + .../core/v1alpha1/zz_generated.conversion.go | 2 + .../core/v1alpha1/zz_generated.deepcopy.go | 7 +- pkg/apis/core/zz_generated.deepcopy.go | 7 +- pkg/apis/openapi/openapi_generated.go | 7 + pkg/controllers/namespaceregistration/add.go | 6 +- .../namespaceregistration/controller.go | 230 ++++++++-- .../namespaceregistration/deletion_handler.go | 112 +++++ .../namespaceregistration/reconcile_test.go | 399 ++++++++++++++++-- .../namespaceregistration.yaml | 2 +- .../test4/namespaceregistration.yaml | 5 + .../test5/namespaceregistration.yaml | 5 + .../test6/namespaceregistration.yaml | 5 + .../test7/namespaceregistration.yaml | 5 + .../test8/namespaceregistration.yaml | 7 + .../test9/namespaceregistration.yaml | 7 + pkg/controllers/subjectsync/add.go | 2 +- pkg/controllers/subjectsync/controller.go | 4 +- ...gardener.cloud_namespaceregistrations.yaml | 30 ++ pkg/utils/utils.go | 20 + test/utils/controller.go | 5 +- test/utils/envtest/environment.go | 24 +- test/utils/envtest/state.go | 27 ++ test/utils/envtest/types.go | 12 + .../apis/core/v1alpha1/helper/dataobjects.go | 94 +++++ .../apis/core/v1alpha1/helper/helpers.go | 254 +++++++++++ .../core/v1alpha1/helper/installationstate.go | 18 + vendor/modules.txt | 1 + 42 files changed, 1402 insertions(+), 99 deletions(-) create mode 100644 docs/usage/Namespaceregistration.md create mode 100644 pkg/controllers/namespaceregistration/deletion_handler.go rename pkg/controllers/namespaceregistration/testdata/reconcile/{test2 => test3}/namespaceregistration.yaml (81%) create mode 100644 pkg/controllers/namespaceregistration/testdata/reconcile/test4/namespaceregistration.yaml create mode 100644 pkg/controllers/namespaceregistration/testdata/reconcile/test5/namespaceregistration.yaml create mode 100644 pkg/controllers/namespaceregistration/testdata/reconcile/test6/namespaceregistration.yaml create mode 100644 pkg/controllers/namespaceregistration/testdata/reconcile/test7/namespaceregistration.yaml create mode 100644 pkg/controllers/namespaceregistration/testdata/reconcile/test8/namespaceregistration.yaml create mode 100644 pkg/controllers/namespaceregistration/testdata/reconcile/test9/namespaceregistration.yaml create mode 100644 vendor/github.com/gardener/landscaper/apis/core/v1alpha1/helper/dataobjects.go create mode 100644 vendor/github.com/gardener/landscaper/apis/core/v1alpha1/helper/helpers.go create mode 100644 vendor/github.com/gardener/landscaper/apis/core/v1alpha1/helper/installationstate.go diff --git a/Makefile b/Makefile index 9ac22475f..3d0fee2a1 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,10 @@ format: @$(REPO_ROOT)/hack/format.sh $(REPO_ROOT)/pkg $(REPO_ROOT)/cmd $(REPO_ROOT)/hack $(REPO_ROOT)/test $(REPO_ROOT)/integration-test/pkg .PHONY: check -check: revendor +check: revendor check-fast + +.PHONY: check-fast +check-fast: @$(REPO_ROOT)/hack/check.sh --golangci-lint-config=./.golangci.yaml $(REPO_ROOT)/cmd/... $(REPO_ROOT)/pkg/... $(REPO_ROOT)/hack/... $(REPO_ROOT)/test/... @cd $(REPO_ROOT)/integration-test && $(REPO_ROOT)/hack/check.sh --golangci-lint-config=$(REPO_ROOT)/.golangci.yaml ./pkg/... diff --git a/docs/architecture/architecture.md b/docs/architecture/architecture.md index e31730ea6..45d41c5e6 100644 --- a/docs/architecture/architecture.md +++ b/docs/architecture/architecture.md @@ -248,21 +248,8 @@ The following image gives a more detailed descriptions of the involved roles, cl #### Controlling Customer Namespaces on the Resource-Shoot-Cluster A user, with access to the Resource-Shoot-Cluster as described before, is only allowed to create Landscaper resources -like Installations, Targets etc. in so-called customer namespaces. A customer namespace is a normal namespace on the -Resource-Shoot-Cluster with a name starting with the prefix *cu-*. - -To create such a namespace the user must create a -*[namespaceRegistration](../../pkg/apis/core/v1alpha1/types_namespaceregistration.go)* object in the namespace ls-user -with the same name as the namespace. The following manifest for example would create a customer namespace *cu-test*: - -```yaml -apiVersion: landscaper-service.gardener.cloud/v1alpha1 -kind: NamespaceRegistration -metadata: - name: cu-test - namespace: ls-user -spec: {} -``` +like Installations, Targets etc. in so-called customer namespaces. More about customer namespaces could be found +[here](../usage/Namespaceregistration.md) The controllers of ls-service-target-shoot-sidecar-server automatically creates the required roles, role-bindings etc. for all entries in the `SubjectList` *subjects* in every newly created customer namespace (see the details in the image diff --git a/docs/usage/Namespaceregistration.md b/docs/usage/Namespaceregistration.md new file mode 100644 index 000000000..91db546f3 --- /dev/null +++ b/docs/usage/Namespaceregistration.md @@ -0,0 +1,87 @@ +# NamespaceRegistrations + +A user, with access to the Resource-Shoot-Cluster as described before, is only allowed to create Landscaper resources +like Installations, Targets etc. in so-called customer namespaces. A customer namespace is a normal namespace on the +Resource-Shoot-Cluster with a name starting with the prefix `cu-`. + +## Creating a Customer Namespace + +To create such a customer namespace the user must create a +*[NamespaceRegistration](../../pkg/apis/core/v1alpha1/types_namespaceregistration.go)* object in the namespace `ls-user` +with the same name as the namespace. The following manifest for example would create a customer namespace `cu-test`: + +```yaml +apiVersion: landscaper-service.gardener.cloud/v1alpha1 +kind: NamespaceRegistration +metadata: + name: cu-test + namespace: ls-user +spec: {} +``` + +When the creation of a customer namespace starts, the status of the `NamespaceRegistration` looks as follows: + +```yaml +status: + phase: Creating +``` + +If the creation of a customer namespace was successful, the status of the `NamespaceRegistration` looks as follows: + +```yaml +status: + phase: Completed +``` + +If the creation of a customer namespace fails, the status of the `NamespaceRegistration` looks as follows: + +```yaml +status: + phase: Failed +``` + +In case of an error, you find the last error also in the status section: + +```yaml +status: + lastError: ... +``` + +If during the namespace creation a potentially sporadic error occurs, the creation operation is retried after 30 seconds. + +## Deleting NamespaceRegistrations + +When deleting a `NamespaceRegistration` the corresponding namespace is deleted. There are three different deletion +strategies depending on the annotation `landscaper-service.gardener.cloud/on-delete-strategy` of the `NamespaceRegistration`: + +- **No annotation (default strategy)**: + - All root Installations with a "delete-without-uninstall" annotation + ([see](https://github.com/gardener/landscaper/blob/master/docs/usage/Annotations.md#delete-without-uninstall-annotation)) + are deleted. + - As long as there are still Installations in the namespace, the namespace is not deleted and this is written + into the field `status.lastError` of the `NamespaceRegistration`. This also means, if there are still installations + without a "delete-without-uninstall" annotation, these have to be deleted by the customer itself. + - Is there are no Installations in the namespace anymore, all other resources in that namespace are removed and + subsequently the namespace is deleted. If the customer has created resources with a custom finalizer, these have to be + removed before deleting a `NamespaceRegistration`. Otherwise, the final deletion might fail and requires manual + intervention. It is anyhow no good idea and should be prevented to create resources with custom finalizers in + a customer namespace. + - If something fails or installations are still in the namespace, the deletion is retried every 30 seconds. + - When the namespace has been deleted, the finalizer of the `NamespaceRegistration` is removed. + +- **Annotation "landscaper-service.gardener.cloud/on-delete-strategy=delete-all-installations"**: + - Same as the default strategy, but all root installations are deleted instead of only those with a + "delete-without-uninstall" annotation. + +- **Annotation "landscaper-service.gardener.cloud/on-delete-strategy=delete-all-installations-without-uninstall"**: + - Same as the default strategy, but in a first step all root installations are annotated with the + "delete-without-uninstall" annotation. + +When the deletion started, the status of the `NamespaceRegistration` looks as follows: + +```yaml +status: + phase: Deleting +``` + +Potential problems are again stored in the field `status.lastError`. \ No newline at end of file diff --git a/hack/setup-testenv.sh b/hack/setup-testenv.sh index 5298e0467..763c23917 100755 --- a/hack/setup-testenv.sh +++ b/hack/setup-testenv.sh @@ -30,7 +30,7 @@ ln -s "${KUBEBUILDER_ASSETS}" ${PROJECT_ROOT}/tmp/test/bin LANDSCAPER_APIS_VERSION=$(go list -m -mod=readonly -f {{.Version}} github.com/gardener/landscaper/apis) LANDSCAPER_CRD_URL="https://raw.githubusercontent.com/gardener/landscaper/${LANDSCAPER_APIS_VERSION}/pkg/landscaper/crdmanager/crdresources" LANDSCAPER_CRD_DIR="${PROJECT_ROOT}/tmp/landscapercrd" -LANDSCAPER_CRDS="landscaper.gardener.cloud_installations.yaml landscaper.gardener.cloud_targets.yaml landscaper.gardener.cloud_dataobjects.yaml landscaper.gardener.cloud_contexts.yaml landscaper.gardener.cloud_lshealthchecks.yaml" +LANDSCAPER_CRDS="landscaper.gardener.cloud_installations.yaml landscaper.gardener.cloud_executions.yaml landscaper.gardener.cloud_deployitems.yaml landscaper.gardener.cloud_targetsyncs.yaml landscaper.gardener.cloud_targets.yaml landscaper.gardener.cloud_dataobjects.yaml landscaper.gardener.cloud_contexts.yaml landscaper.gardener.cloud_lshealthchecks.yaml" mkdir -p ${PROJECT_ROOT}/tmp/landscapercrd for crd in $LANDSCAPER_CRDS; do diff --git a/integration-test/vendor/github.com/gardener/landscaper-service/pkg/apis/core/types_namespaceregistration.go b/integration-test/vendor/github.com/gardener/landscaper-service/pkg/apis/core/types_namespaceregistration.go index ab338bba8..bf82a6f54 100644 --- a/integration-test/vendor/github.com/gardener/landscaper-service/pkg/apis/core/types_namespaceregistration.go +++ b/integration-test/vendor/github.com/gardener/landscaper-service/pkg/apis/core/types_namespaceregistration.go @@ -34,6 +34,8 @@ type NamespaceRegistration struct { type NamespaceRegistrationStatus struct { Phase string `json:"phase"` + // +optional + LastError *Error `json:"lastError,omitempty"` } type NamespaceRegistrationSpec struct { diff --git a/integration-test/vendor/github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1/constants.go b/integration-test/vendor/github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1/constants.go index bedab2933..82473d90f 100644 --- a/integration-test/vendor/github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1/constants.go +++ b/integration-test/vendor/github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1/constants.go @@ -19,4 +19,8 @@ const ( // When set at landscaper deployments, the annotation will be inherited to the corresponding instance // and prevents its reconciliation until removed. LandscaperServiceOperationIgnore = "ignore" + + LandscaperServiceOnDeleteStrategyAnnotation = "landscaper-service.gardener.cloud/on-delete-strategy" + LandscaperServiceOnDeleteStrategyDeleteAllInstallations = "delete-all-installations" + LandscaperServiceOnDeleteStrategyDeleteAllInstallationsWithoutUninstall = "delete-all-installations-without-uninstall" ) diff --git a/integration-test/vendor/github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1/types_namespaceregistration.go b/integration-test/vendor/github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1/types_namespaceregistration.go index 960e6cb14..c89524884 100644 --- a/integration-test/vendor/github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1/types_namespaceregistration.go +++ b/integration-test/vendor/github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1/types_namespaceregistration.go @@ -35,6 +35,8 @@ type NamespaceRegistration struct { type NamespaceRegistrationStatus struct { Phase string `json:"phase"` + // +optional + LastError *Error `json:"lastError,omitempty"` } type NamespaceRegistrationSpec struct { diff --git a/integration-test/vendor/github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1/zz_generated.conversion.go b/integration-test/vendor/github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1/zz_generated.conversion.go index 97dabb314..5a64e72d9 100644 --- a/integration-test/vendor/github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1/zz_generated.conversion.go +++ b/integration-test/vendor/github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1/zz_generated.conversion.go @@ -933,6 +933,7 @@ func Convert_core_NamespaceRegistrationSpec_To_v1alpha1_NamespaceRegistrationSpe func autoConvert_v1alpha1_NamespaceRegistrationStatus_To_core_NamespaceRegistrationStatus(in *NamespaceRegistrationStatus, out *core.NamespaceRegistrationStatus, s conversion.Scope) error { out.Phase = in.Phase + out.LastError = (*core.Error)(unsafe.Pointer(in.LastError)) return nil } @@ -943,6 +944,7 @@ func Convert_v1alpha1_NamespaceRegistrationStatus_To_core_NamespaceRegistrationS func autoConvert_core_NamespaceRegistrationStatus_To_v1alpha1_NamespaceRegistrationStatus(in *core.NamespaceRegistrationStatus, out *NamespaceRegistrationStatus, s conversion.Scope) error { out.Phase = in.Phase + out.LastError = (*Error)(unsafe.Pointer(in.LastError)) return nil } diff --git a/integration-test/vendor/github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1/zz_generated.deepcopy.go b/integration-test/vendor/github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1/zz_generated.deepcopy.go index 1d8e26612..86718f709 100644 --- a/integration-test/vendor/github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1/zz_generated.deepcopy.go +++ b/integration-test/vendor/github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1/zz_generated.deepcopy.go @@ -485,7 +485,7 @@ func (in *NamespaceRegistration) DeepCopyInto(out *NamespaceRegistration) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) out.Spec = in.Spec - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -559,6 +559,11 @@ func (in *NamespaceRegistrationSpec) DeepCopy() *NamespaceRegistrationSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NamespaceRegistrationStatus) DeepCopyInto(out *NamespaceRegistrationStatus) { *out = *in + if in.LastError != nil { + in, out := &in.LastError, &out.LastError + *out = new(Error) + (*in).DeepCopyInto(*out) + } return } diff --git a/integration-test/vendor/github.com/gardener/landscaper-service/pkg/apis/core/zz_generated.deepcopy.go b/integration-test/vendor/github.com/gardener/landscaper-service/pkg/apis/core/zz_generated.deepcopy.go index 67d5d1698..3b0b2c6fe 100644 --- a/integration-test/vendor/github.com/gardener/landscaper-service/pkg/apis/core/zz_generated.deepcopy.go +++ b/integration-test/vendor/github.com/gardener/landscaper-service/pkg/apis/core/zz_generated.deepcopy.go @@ -485,7 +485,7 @@ func (in *NamespaceRegistration) DeepCopyInto(out *NamespaceRegistration) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) out.Spec = in.Spec - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -559,6 +559,11 @@ func (in *NamespaceRegistrationSpec) DeepCopy() *NamespaceRegistrationSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NamespaceRegistrationStatus) DeepCopyInto(out *NamespaceRegistrationStatus) { *out = *in + if in.LastError != nil { + in, out := &in.LastError, &out.LastError + *out = new(Error) + (*in).DeepCopyInto(*out) + } return } diff --git a/integration-test/vendor/github.com/gardener/landscaper-service/pkg/utils/utils.go b/integration-test/vendor/github.com/gardener/landscaper-service/pkg/utils/utils.go index 2727a89c2..cbb823782 100644 --- a/integration-test/vendor/github.com/gardener/landscaper-service/pkg/utils/utils.go +++ b/integration-test/vendor/github.com/gardener/landscaper-service/pkg/utils/utils.go @@ -8,6 +8,8 @@ import ( "fmt" "strconv" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" lsv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" @@ -84,3 +86,21 @@ func RemoveOperationAnnotation(object client.Object) { delete(annotations, lssv1alpha1.LandscaperServiceOperationAnnotation) } } + +// HasLabel checks if the objects has a label +func HasLabel(obj metav1.Object, lab string) bool { + labels := obj.GetLabels() + if labels == nil { + return false + } + _, ok := labels[lab] + return ok +} + +// HasDeleteWithoutUninstallAnnotation returns true only if the given object +// has the 'landscaper.gardener.cloud/delete-without-uninstall' annotation +// and its value is 'true'. +func HasDeleteWithoutUninstallAnnotation(obj metav1.Object) bool { + v, ok := obj.GetAnnotations()[lsv1alpha1.DeleteWithoutUninstallAnnotation] + return ok && v == "true" +} diff --git a/pkg/apis/.schemes/core-v1alpha1-NamespaceRegistration.json b/pkg/apis/.schemes/core-v1alpha1-NamespaceRegistration.json index d6337afde..822e6b4fd 100755 --- a/pkg/apis/.schemes/core-v1alpha1-NamespaceRegistration.json +++ b/pkg/apis/.schemes/core-v1alpha1-NamespaceRegistration.json @@ -1,6 +1,44 @@ { "$schema": "https://json-schema.org/draft-07/schema#", "definitions": { + "core-v1alpha1-Error": { + "description": "Error holds information about an error that occurred.", + "type": "object", + "required": [ + "operation", + "lastTransitionTime", + "lastUpdateTime", + "reason", + "message" + ], + "properties": { + "lastTransitionTime": { + "description": "Last time the condition transitioned from one status to another.", + "default": {}, + "$ref": "#/definitions/meta-v1-Time" + }, + "lastUpdateTime": { + "description": "Last time the condition was updated.", + "default": {}, + "$ref": "#/definitions/meta-v1-Time" + }, + "message": { + "description": "A human-readable message indicating details about the transition.", + "type": "string", + "default": "" + }, + "operation": { + "description": "Operation describes the operator where the error occurred.", + "type": "string", + "default": "" + }, + "reason": { + "description": "The reason for the condition's last transition.", + "type": "string", + "default": "" + } + } + }, "core-v1alpha1-NamespaceRegistrationSpec": { "type": "object" }, @@ -10,6 +48,9 @@ "phase" ], "properties": { + "lastError": { + "$ref": "#/definitions/core-v1alpha1-Error" + }, "phase": { "type": "string", "default": "" diff --git a/pkg/apis/core/types_namespaceregistration.go b/pkg/apis/core/types_namespaceregistration.go index ab338bba8..bf82a6f54 100644 --- a/pkg/apis/core/types_namespaceregistration.go +++ b/pkg/apis/core/types_namespaceregistration.go @@ -34,6 +34,8 @@ type NamespaceRegistration struct { type NamespaceRegistrationStatus struct { Phase string `json:"phase"` + // +optional + LastError *Error `json:"lastError,omitempty"` } type NamespaceRegistrationSpec struct { diff --git a/pkg/apis/core/v1alpha1/constants.go b/pkg/apis/core/v1alpha1/constants.go index bedab2933..82473d90f 100644 --- a/pkg/apis/core/v1alpha1/constants.go +++ b/pkg/apis/core/v1alpha1/constants.go @@ -19,4 +19,8 @@ const ( // When set at landscaper deployments, the annotation will be inherited to the corresponding instance // and prevents its reconciliation until removed. LandscaperServiceOperationIgnore = "ignore" + + LandscaperServiceOnDeleteStrategyAnnotation = "landscaper-service.gardener.cloud/on-delete-strategy" + LandscaperServiceOnDeleteStrategyDeleteAllInstallations = "delete-all-installations" + LandscaperServiceOnDeleteStrategyDeleteAllInstallationsWithoutUninstall = "delete-all-installations-without-uninstall" ) diff --git a/pkg/apis/core/v1alpha1/types_namespaceregistration.go b/pkg/apis/core/v1alpha1/types_namespaceregistration.go index 960e6cb14..c89524884 100644 --- a/pkg/apis/core/v1alpha1/types_namespaceregistration.go +++ b/pkg/apis/core/v1alpha1/types_namespaceregistration.go @@ -35,6 +35,8 @@ type NamespaceRegistration struct { type NamespaceRegistrationStatus struct { Phase string `json:"phase"` + // +optional + LastError *Error `json:"lastError,omitempty"` } type NamespaceRegistrationSpec struct { diff --git a/pkg/apis/core/v1alpha1/zz_generated.conversion.go b/pkg/apis/core/v1alpha1/zz_generated.conversion.go index 97dabb314..5a64e72d9 100644 --- a/pkg/apis/core/v1alpha1/zz_generated.conversion.go +++ b/pkg/apis/core/v1alpha1/zz_generated.conversion.go @@ -933,6 +933,7 @@ func Convert_core_NamespaceRegistrationSpec_To_v1alpha1_NamespaceRegistrationSpe func autoConvert_v1alpha1_NamespaceRegistrationStatus_To_core_NamespaceRegistrationStatus(in *NamespaceRegistrationStatus, out *core.NamespaceRegistrationStatus, s conversion.Scope) error { out.Phase = in.Phase + out.LastError = (*core.Error)(unsafe.Pointer(in.LastError)) return nil } @@ -943,6 +944,7 @@ func Convert_v1alpha1_NamespaceRegistrationStatus_To_core_NamespaceRegistrationS func autoConvert_core_NamespaceRegistrationStatus_To_v1alpha1_NamespaceRegistrationStatus(in *core.NamespaceRegistrationStatus, out *NamespaceRegistrationStatus, s conversion.Scope) error { out.Phase = in.Phase + out.LastError = (*Error)(unsafe.Pointer(in.LastError)) return nil } diff --git a/pkg/apis/core/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/core/v1alpha1/zz_generated.deepcopy.go index 1d8e26612..86718f709 100644 --- a/pkg/apis/core/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/core/v1alpha1/zz_generated.deepcopy.go @@ -485,7 +485,7 @@ func (in *NamespaceRegistration) DeepCopyInto(out *NamespaceRegistration) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) out.Spec = in.Spec - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -559,6 +559,11 @@ func (in *NamespaceRegistrationSpec) DeepCopy() *NamespaceRegistrationSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NamespaceRegistrationStatus) DeepCopyInto(out *NamespaceRegistrationStatus) { *out = *in + if in.LastError != nil { + in, out := &in.LastError, &out.LastError + *out = new(Error) + (*in).DeepCopyInto(*out) + } return } diff --git a/pkg/apis/core/zz_generated.deepcopy.go b/pkg/apis/core/zz_generated.deepcopy.go index 67d5d1698..3b0b2c6fe 100644 --- a/pkg/apis/core/zz_generated.deepcopy.go +++ b/pkg/apis/core/zz_generated.deepcopy.go @@ -485,7 +485,7 @@ func (in *NamespaceRegistration) DeepCopyInto(out *NamespaceRegistration) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) out.Spec = in.Spec - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -559,6 +559,11 @@ func (in *NamespaceRegistrationSpec) DeepCopy() *NamespaceRegistrationSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NamespaceRegistrationStatus) DeepCopyInto(out *NamespaceRegistrationStatus) { *out = *in + if in.LastError != nil { + in, out := &in.LastError, &out.LastError + *out = new(Error) + (*in).DeepCopyInto(*out) + } return } diff --git a/pkg/apis/openapi/openapi_generated.go b/pkg/apis/openapi/openapi_generated.go index 9d75bba6c..9bf2d4289 100644 --- a/pkg/apis/openapi/openapi_generated.go +++ b/pkg/apis/openapi/openapi_generated.go @@ -1382,10 +1382,17 @@ func schema_pkg_apis_core_v1alpha1_NamespaceRegistrationStatus(ref common.Refere Format: "", }, }, + "lastError": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1.Error"), + }, + }, }, Required: []string{"phase"}, }, }, + Dependencies: []string{ + "github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1.Error"}, } } diff --git a/pkg/controllers/namespaceregistration/add.go b/pkg/controllers/namespaceregistration/add.go index 907ec847a..1ed41dab1 100644 --- a/pkg/controllers/namespaceregistration/add.go +++ b/pkg/controllers/namespaceregistration/add.go @@ -8,6 +8,7 @@ import ( "github.com/go-logr/logr" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/gardener/landscaper/controller-utils/pkg/logging" @@ -24,8 +25,11 @@ func AddControllerToManager(logger logging.Logger, mgr manager.Manager, config * return err } + predicates := builder.WithPredicates(predicate.Or(predicate.LabelChangedPredicate{}, + predicate.GenerationChangedPredicate{}, predicate.AnnotationChangedPredicate{})) + return builder.ControllerManagedBy(mgr). - For(&v1alpha1.NamespaceRegistration{}). + For(&v1alpha1.NamespaceRegistration{}, predicates). WithLogConstructor(func(r *reconcile.Request) logr.Logger { return log.Logr() }). Complete(ctrl) } diff --git a/pkg/controllers/namespaceregistration/controller.go b/pkg/controllers/namespaceregistration/controller.go index 1f7a8c0b2..b0a210661 100644 --- a/pkg/controllers/namespaceregistration/controller.go +++ b/pkg/controllers/namespaceregistration/controller.go @@ -8,7 +8,9 @@ import ( "context" "fmt" "strings" + "time" + "github.com/gardener/landscaper/apis/core/v1alpha1" kutils "github.com/gardener/landscaper/controller-utils/pkg/kubernetes" "github.com/gardener/landscaper/controller-utils/pkg/logging" corev1 "k8s.io/api/core/v1" @@ -25,6 +27,18 @@ import ( lssv1alpha1 "github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1" "github.com/gardener/landscaper-service/pkg/controllers/subjectsync" "github.com/gardener/landscaper-service/pkg/operation" + "github.com/gardener/landscaper-service/pkg/utils" +) + +const ( + PhaseCreating = "Creating" + PhaseFailed = "Failed" + PhaseCompleted = "Completed" + PhaseDeleting = "Deleting" + + ReasonInvalidName = "invalid name" + + requeueAfterDuration = 30 * time.Second ) type Controller struct { @@ -60,20 +74,29 @@ func NewTestActuator(op operation.TargetShootSidecarOperation, logger logging.Lo func (c *Controller) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { logger, ctx := c.log.StartReconcileAndAddToContext(ctx, req) + logger.Info("start reconcile namespaceRegistration") + namespaceRegistration := &lssv1alpha1.NamespaceRegistration{} if err := c.Client().Get(ctx, req.NamespacedName, namespaceRegistration); err != nil { - logger.Error(err, "failed loading namespaceregistration cr") + logger.Error(err, "failed loading namespaceregistration") if apierrors.IsNotFound(err) { return reconcile.Result{}, nil } - return reconcile.Result{}, err + return reconcile.Result{RequeueAfter: requeueAfterDuration}, nil } if !strings.HasPrefix(namespaceRegistration.Name, subjectsync.CUSTOM_NS_PREFIX) { - namespaceRegistration.Status.Phase = fmt.Sprintf("InvalidName: name must start with %s", subjectsync.CUSTOM_NS_PREFIX) - if err := c.Client().Status().Update(ctx, namespaceRegistration); err != nil { - logger.Error(err, "failed to update namespaceregistration with invalid name - must start with "+subjectsync.CUSTOM_NS_PREFIX) - return reconcile.Result{}, err + if namespaceRegistration.Status.Phase != PhaseFailed || + namespaceRegistration.Status.LastError == nil || + (namespaceRegistration.Status.LastError != nil && namespaceRegistration.Status.LastError.Reason != ReasonInvalidName) { + + err := fmt.Errorf("name must start with %q", subjectsync.CUSTOM_NS_PREFIX) + lastError := c.createError(namespaceRegistration.Status.Phase, ReasonInvalidName, err) + c.updateStatus(namespaceRegistration, PhaseFailed, lastError) + if err := c.Client().Status().Update(ctx, namespaceRegistration); err != nil { + logger.Error(err, "failed updating namespaceregistration with invalid name - must start with "+subjectsync.CUSTOM_NS_PREFIX) + return reconcile.Result{RequeueAfter: requeueAfterDuration}, nil + } } return reconcile.Result{}, nil @@ -83,9 +106,10 @@ func (c *Controller) Reconcile(ctx context.Context, req reconcile.Request) (reco if namespaceRegistration.DeletionTimestamp.IsZero() && !kutils.HasFinalizer(namespaceRegistration, lssv1alpha1.LandscaperServiceFinalizer) { controllerutil.AddFinalizer(namespaceRegistration, lssv1alpha1.LandscaperServiceFinalizer) if err := c.Client().Update(ctx, namespaceRegistration); err != nil { - return reconcile.Result{}, err + logger.Error(err, "failed adding finalizer to namespaceregistration") + return reconcile.Result{RequeueAfter: requeueAfterDuration}, nil } - return reconcile.Result{}, nil + // do not return here because the controller only watches for particular events and setting a finalizer is not part of this } // reconcile delete @@ -106,66 +130,144 @@ func (c *Controller) handleDelete(ctx context.Context, namespaceRegistration *ls controllerutil.RemoveFinalizer(namespaceRegistration, lssv1alpha1.LandscaperServiceFinalizer) if err := c.Client().Update(ctx, namespaceRegistration); err != nil { logger.Error(err, "failed removing finalizer") - return reconcile.Result{}, err + return reconcile.Result{RequeueAfter: requeueAfterDuration}, nil } return reconcile.Result{}, nil } - logger.Error(err, "failed loading namespace cr") - return reconcile.Result{}, err + logger.Error(err, "failed loading namespace") + return reconcile.Result{RequeueAfter: requeueAfterDuration}, nil + } + + return c.removeResourcesAndNamespace(ctx, namespaceRegistration, namespace) +} + +func (c *Controller) removeResourcesAndNamespace(ctx context.Context, namespaceRegistration *lssv1alpha1.NamespaceRegistration, + namespace *corev1.Namespace) (reconcile.Result, error) { + + logger, ctx := logging.FromContextOrNew(ctx, nil) + + if namespaceRegistration.Status.Phase != PhaseDeleting { + c.updateStatus(namespaceRegistration, PhaseDeleting, nil) + if err := c.Client().Status().Update(ctx, namespaceRegistration); err != nil { + logger.Error(err, "failed updating status of namespaceregistration when starting deletion") + return reconcile.Result{RequeueAfter: requeueAfterDuration}, nil + } + } + + // check if installations, executions, deploy items or target sync objects are still there + installations := &v1alpha1.InstallationList{} + if err := c.Client().List(ctx, installations, client.InNamespace(namespaceRegistration.GetName())); err != nil { + return c.logErrorUpdateAndRetry(ctx, namespaceRegistration, PhaseDeleting, "failed reading installations", err) + } + + if len(installations.Items) > 0 { + err := c.triggerDeletionOfInstallations(ctx, namespaceRegistration, installations.Items) + if err != nil { + return c.logErrorUpdateAndRetry(ctx, namespaceRegistration, PhaseDeleting, "failed deleting installations", err) + } + + return c.logErrorUpdateAndRetry(ctx, namespaceRegistration, PhaseDeleting, "namespace contains installations", nil) + } + + executions := &v1alpha1.ExecutionList{} + if err := c.Client().List(ctx, executions, client.InNamespace(namespaceRegistration.GetName())); err != nil { + return c.logErrorUpdateAndRetry(ctx, namespaceRegistration, PhaseDeleting, "failed reading executions", err) + } + + if len(executions.Items) > 0 { + return c.logErrorUpdateAndRetry(ctx, namespaceRegistration, PhaseDeleting, "namespace contains executions", nil) + } + + deployItems := &v1alpha1.DeployItemList{} + if err := c.Client().List(ctx, deployItems, client.InNamespace(namespaceRegistration.GetName())); err != nil { + return c.logErrorUpdateAndRetry(ctx, namespaceRegistration, PhaseDeleting, "failed reading deploy items", err) + } + + if len(deployItems.Items) > 0 { + return c.logErrorUpdateAndRetry(ctx, namespaceRegistration, PhaseDeleting, "namespace contains deploy items", nil) + } + + targetSyncs := &v1alpha1.TargetSyncList{} + if err := c.Client().List(ctx, targetSyncs, client.InNamespace(namespaceRegistration.GetName())); err != nil { + return c.logErrorUpdateAndRetry(ctx, namespaceRegistration, PhaseDeleting, "failed reading targetsyncs", err) + } + + if len(targetSyncs.Items) > 0 { + for i := range targetSyncs.Items { + nextTargetSync := &targetSyncs.Items[i] + if err := c.Client().Delete(ctx, nextTargetSync); err != nil { + return c.logErrorUpdateAndRetry(ctx, namespaceRegistration, PhaseDeleting, "failed removing targetsync", err) + } + } + + return c.logErrorUpdateAndRetry(ctx, namespaceRegistration, PhaseDeleting, "namespace contains targetsyncs", nil) } - //delete role binding + return c.removeAccessDataAndNamespace(ctx, namespaceRegistration, namespace) +} + +func (c *Controller) removeAccessDataAndNamespace(ctx context.Context, namespaceRegistration *lssv1alpha1.NamespaceRegistration, + namespace *corev1.Namespace) (reconcile.Result, error) { + + logger, ctx := logging.FromContextOrNew(ctx, nil) + + // delete role binding roleBinding := &rbacv1.RoleBinding{} if err := c.Client().Get(ctx, types.NamespacedName{Name: subjectsync.USER_ROLE_BINDING_IN_NAMESPACE, Namespace: namespaceRegistration.GetName()}, roleBinding); err != nil { if apierrors.IsNotFound(err) { logger.Info("rolebinding in namespace not found") } else { - logger.Error(err, "failed loading rolebinding in namespace") - return reconcile.Result{}, err + return c.logErrorUpdateAndRetry(ctx, namespaceRegistration, PhaseDeleting, "failed loading rolebinding", err) } } else { if err := c.Client().Delete(ctx, roleBinding); err != nil { - logger.Error(err, "failed deleting rolebinding in namespace") - return reconcile.Result{}, err //TODO + return c.logErrorUpdateAndRetry(ctx, namespaceRegistration, PhaseDeleting, "failed deleting rolebinding", err) } } + //delete role role := &rbacv1.Role{} if err := c.Client().Get(ctx, types.NamespacedName{Name: subjectsync.USER_ROLE_IN_NAMESPACE, Namespace: namespaceRegistration.GetName()}, role); err != nil { if apierrors.IsNotFound(err) { logger.Info("role in namespace not found") } else { - logger.Error(err, "failed loading role in namespace") - return reconcile.Result{}, err + return c.logErrorUpdateAndRetry(ctx, namespaceRegistration, PhaseDeleting, "failed loading role", err) } } else { if err := c.Client().Delete(ctx, role); err != nil { - logger.Error(err, "failed deleting role in namespace") - return reconcile.Result{}, err //TODO + return c.logErrorUpdateAndRetry(ctx, namespaceRegistration, PhaseDeleting, "failed deleting role", err) } } + // delete namespace if err := c.Client().Delete(ctx, namespace); err != nil { - logger.Error(err, "failed loading namespace cr") - return reconcile.Result{}, err + return c.logErrorUpdateAndRetry(ctx, namespaceRegistration, PhaseDeleting, "failed deleting namespace", err) } + controllerutil.RemoveFinalizer(namespaceRegistration, lssv1alpha1.LandscaperServiceFinalizer) if err := c.Client().Update(ctx, namespaceRegistration); err != nil { - logger.Error(err, "failed removing finalizer") - return reconcile.Result{}, err + return c.logErrorUpdateAndRetry(ctx, namespaceRegistration, PhaseDeleting, "failed removing finalizer", err) } - return reconcile.Result{}, nil + return reconcile.Result{}, nil } func (c *Controller) reconcile(ctx context.Context, namespaceRegistration *lssv1alpha1.NamespaceRegistration) (reconcile.Result, error) { logger, ctx := logging.FromContextOrNew(ctx, nil) - if namespaceRegistration.Status.Phase == "Completed" { - logger.Info("Phase already in Completed") + if namespaceRegistration.Status.Phase == PhaseCompleted { + logger.Debug("Phase already in Completed") return reconcile.Result{}, nil } + if namespaceRegistration.Status.Phase == "" { + c.updateStatus(namespaceRegistration, PhaseCreating, namespaceRegistration.Status.LastError) + if err := c.Client().Status().Update(ctx, namespaceRegistration); err != nil { + logger.Error(err, "failed updating status of namespaceregistration when starting namespace creation") + return reconcile.Result{RequeueAfter: requeueAfterDuration}, nil + } + } + namespace := &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: namespaceRegistration.Name, @@ -174,22 +276,22 @@ func (c *Controller) reconcile(ctx context.Context, namespaceRegistration *lssv1 if err := c.Client().Create(ctx, namespace); err != nil { if !apierrors.IsAlreadyExists(err) { - logger.Error(err, "failed creating namespace") - return reconcile.Result{}, err + return c.logErrorUpdateAndRetry(ctx, namespaceRegistration, PhaseCreating, "failed creating namespace", err) } } if err := c.createRoleIfNotExistOrUpdate(ctx, namespaceRegistration); err != nil { - namespaceRegistration.Status.Phase = "Failed Role Creation" + return c.logErrorUpdateAndRetry(ctx, namespaceRegistration, PhaseCreating, "failed creating role", err) } + if err := c.createRoleBindingIfNotExistOrUpdate(ctx, namespaceRegistration); err != nil { - namespaceRegistration.Status.Phase = "Failed Role Binding" + return c.logErrorUpdateAndRetry(ctx, namespaceRegistration, PhaseCreating, "failed creating rolebinding", err) } - namespaceRegistration.Status.Phase = "Completed" + c.updateStatus(namespaceRegistration, PhaseCompleted, nil) if err := c.Client().Status().Update(ctx, namespaceRegistration); err != nil { - logger.Error(err, "failed updating status of namespaceregistration") - return reconcile.Result{}, err + logger.Error(err, "failed updating status of namespaceregistration after completion") + return reconcile.Result{RequeueAfter: requeueAfterDuration}, nil } return reconcile.Result{}, nil } @@ -266,3 +368,63 @@ func (c *Controller) createRoleBindingIfNotExistOrUpdate(ctx context.Context, na return nil } + +func (c *Controller) triggerDeletionOfInstallations(ctx context.Context, namespaceRegistration *lssv1alpha1.NamespaceRegistration, installations []v1alpha1.Installation) error { + triggerDeletion, err := getTriggerDeletionFunction(ctx, namespaceRegistration) + if err != nil { + return err + } + + // trigger deletion of root installations according to deletion strategy + var triggerErr error + for i := range installations { + inst := &installations[i] + if !utils.HasLabel(&inst.ObjectMeta, v1alpha1.EncompassedByLabel) { + if tmpErr := triggerDeletion(ctx, c.Client(), inst); tmpErr != nil { + triggerErr = tmpErr + } + } + } + + return triggerErr +} + +func (c *Controller) logErrorUpdateAndRetry(ctx context.Context, namespaceRegistration *lssv1alpha1.NamespaceRegistration, + phase, msg string, err error) (reconcile.Result, error) { + logger, ctx := logging.FromContextOrNew(ctx, nil) + + if err != nil { + logger.Error(err, msg) + } else { + logger.Info(msg) + } + + lastError := c.createError(namespaceRegistration.Status.Phase, msg, err) + c.updateStatus(namespaceRegistration, phase, lastError) + if err := c.Client().Status().Update(ctx, namespaceRegistration); err != nil { + logger.Error(err, "failed updating status of namespaceregistration after error: "+msg) + } + + return reconcile.Result{RequeueAfter: requeueAfterDuration}, nil +} + +func (c *Controller) updateStatus(namespaceRegistration *lssv1alpha1.NamespaceRegistration, phase string, + lastError *lssv1alpha1.Error) { + namespaceRegistration.Status.Phase = phase + namespaceRegistration.Status.LastError = lastError +} + +func (c *Controller) createError(phase, reason string, err error) *lssv1alpha1.Error { + msg := "" + if err != nil { + msg = err.Error() + } + + return &lssv1alpha1.Error{ + Operation: phase, + LastTransitionTime: metav1.Now(), + LastUpdateTime: metav1.Now(), + Reason: reason, + Message: msg, + } +} diff --git a/pkg/controllers/namespaceregistration/deletion_handler.go b/pkg/controllers/namespaceregistration/deletion_handler.go new file mode 100644 index 000000000..fd8a5b000 --- /dev/null +++ b/pkg/controllers/namespaceregistration/deletion_handler.go @@ -0,0 +1,112 @@ +package namespaceregistration + +import ( + "context" + "fmt" + + "github.com/gardener/landscaper/apis/core/v1alpha1" + "github.com/gardener/landscaper/apis/core/v1alpha1/helper" + "github.com/gardener/landscaper/controller-utils/pkg/logging" + lc "github.com/gardener/landscaper/controller-utils/pkg/logging/constants" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + lssv1alpha1 "github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1" + "github.com/gardener/landscaper-service/pkg/utils" +) + +const keyOnDeleteStrategy = "onDeleteStrategy" + +type triggerDeletionFunc func(ctx context.Context, cl client.Client, inst *v1alpha1.Installation) error + +func getTriggerDeletionFunction(ctx context.Context, namespaceRegistration *lssv1alpha1.NamespaceRegistration) (triggerDeletionFunc, error) { + logger, _ := logging.FromContextOrNew(ctx, nil) + + strategy, found := namespaceRegistration.Annotations[lssv1alpha1.LandscaperServiceOnDeleteStrategyAnnotation] + if !found { + strategy = "" + } + logger.Info("determined on-delete-strategy", keyOnDeleteStrategy, strategy) + + switch strategy { + case "": + return triggerDeletionByDefaultStrategy, nil + case lssv1alpha1.LandscaperServiceOnDeleteStrategyDeleteAllInstallations: + return triggerDeletionWithUninstall, nil + case lssv1alpha1.LandscaperServiceOnDeleteStrategyDeleteAllInstallationsWithoutUninstall: + return triggerDeletionWithoutUninstall, nil + default: + logger.Info("unknown on-delete-strategy", keyOnDeleteStrategy, strategy) + return nil, fmt.Errorf("unknown on-delete-strategy %q", strategy) + } +} + +// triggerDeletionByDefaultStrategy deletes the installation if it is a root installation and +// has the delete-without-uninstall annotation. +func triggerDeletionByDefaultStrategy(ctx context.Context, cl client.Client, inst *v1alpha1.Installation) error { + _, ctx = logging.FromContextOrNew(ctx, nil, + lc.KeyResource, client.ObjectKeyFromObject(inst).String(), + keyOnDeleteStrategy, "") + + if utils.HasDeleteWithoutUninstallAnnotation(&inst.ObjectMeta) { + return deleteOrRetriggerDelete(ctx, cl, inst) + } + + return nil +} + +// triggerDeletionWithUninstall deletes the installation. +func triggerDeletionWithUninstall(ctx context.Context, cl client.Client, inst *v1alpha1.Installation) error { + _, ctx = logging.FromContextOrNew(ctx, nil, + lc.KeyResource, client.ObjectKeyFromObject(inst).String(), + keyOnDeleteStrategy, lssv1alpha1.LandscaperServiceOnDeleteStrategyDeleteAllInstallations) + + return deleteOrRetriggerDelete(ctx, cl, inst) +} + +// triggerDeletionWithoutUninstall deletes the installation without uninstall. +func triggerDeletionWithoutUninstall(ctx context.Context, cl client.Client, inst *v1alpha1.Installation) error { + _, ctx = logging.FromContextOrNew(ctx, nil, + lc.KeyResource, client.ObjectKeyFromObject(inst).String(), + keyOnDeleteStrategy, lssv1alpha1.LandscaperServiceOnDeleteStrategyDeleteAllInstallationsWithoutUninstall) + + if err := ensureDeleteWithoutUninstallAnnotation(ctx, cl, inst); err != nil { + return err + } + + return deleteOrRetriggerDelete(ctx, cl, inst) +} + +func ensureDeleteWithoutUninstallAnnotation(ctx context.Context, cl client.Client, inst *v1alpha1.Installation) error { + logger, ctx := logging.FromContextOrNew(ctx, nil) + + if !utils.HasDeleteWithoutUninstallAnnotation(&inst.ObjectMeta) { + metav1.SetMetaDataAnnotation(&inst.ObjectMeta, v1alpha1.DeleteWithoutUninstallAnnotation, "true") + if err := cl.Update(ctx, inst); err != nil { + logger.Error(err, "failed adding delete-without-uninstall annotation to installation") + return err + } + } + + return nil +} + +func deleteOrRetriggerDelete(ctx context.Context, cl client.Client, inst *v1alpha1.Installation) error { + logger, ctx := logging.FromContextOrNew(ctx, nil) + + if inst.GetDeletionTimestamp().IsZero() { + if err := cl.Delete(ctx, inst); err != nil { + logger.Error(err, "failed deleting installations: "+client.ObjectKeyFromObject(inst).String()) + return err + } + } else if inst.Status.JobID == inst.Status.JobIDFinished && !helper.HasOperation(inst.ObjectMeta, v1alpha1.ReconcileOperation) { + // retrigger + metav1.SetMetaDataAnnotation(&inst.ObjectMeta, v1alpha1.OperationAnnotation, string(v1alpha1.ReconcileOperation)) + if err := cl.Update(ctx, inst); err != nil { + logger.Error(err, "failed annotating installations without uninstall") + return err + } + } + + return nil +} diff --git a/pkg/controllers/namespaceregistration/reconcile_test.go b/pkg/controllers/namespaceregistration/reconcile_test.go index a463f7e2d..4c37ab421 100644 --- a/pkg/controllers/namespaceregistration/reconcile_test.go +++ b/pkg/controllers/namespaceregistration/reconcile_test.go @@ -8,13 +8,17 @@ import ( "context" "time" + lsv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" + "github.com/gardener/landscaper/apis/core/v1alpha1/helper" kutil "github.com/gardener/landscaper/controller-utils/pkg/kubernetes" "github.com/gardener/landscaper/controller-utils/pkg/logging" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/reconcile" lssv1alpha1 "github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1" @@ -46,40 +50,27 @@ var _ = Describe("Reconcile", func() { } }) - It("should add finalizer on reconcile", func() { + It("should create/delete namespace with role and rolebinding on namespaceregistration create/delete", func() { var err error state, err = testenv.InitResources(ctx, "./testdata/reconcile/test1") Expect(err).ToNot(HaveOccurred()) + // reconcile namespaceRegistration := state.GetNamespaceRegistration(subjectsync.CUSTOM_NS_PREFIX + "test-namespace-1") - //reconcile for finalizer testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) + + // check finalizer and phase Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(namespaceRegistration), namespaceRegistration)).To(Succeed()) Expect(len(namespaceRegistration.Finalizers)).To(Equal(1)) Expect(namespaceRegistration.Finalizers[0]).To(Equal(lssv1alpha1.LandscaperServiceFinalizer)) - }) - - It("should create namespace with role/rolebinding on namespaceregistration create", func() { - var err error - - state, err = testenv.InitResources(ctx, "./testdata/reconcile/test1") - Expect(err).ToNot(HaveOccurred()) - - namespaceRegistration := state.GetNamespaceRegistration(subjectsync.CUSTOM_NS_PREFIX + "test-namespace-1") - //reconcile for finalizer - testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) - Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(namespaceRegistration), namespaceRegistration)).To(Succeed()) - //reconcile for actual run - testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) - Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(namespaceRegistration), namespaceRegistration)).To(Succeed()) Expect(namespaceRegistration.Status.Phase).To(Equal("Completed")) // check for namespace being created namespace := corev1.Namespace{} Expect(testenv.Client.Get(ctx, types.NamespacedName{Name: namespaceRegistration.Name}, &namespace)).To(Succeed()) - //check for role being created + // check for role being created role := rbacv1.Role{} Expect(testenv.Client.Get(ctx, types.NamespacedName{Name: subjectsync.USER_ROLE_IN_NAMESPACE, Namespace: namespace.Name}, &role)).To(Succeed()) Expect(role.Rules[0].APIGroups).To(ContainElement("landscaper.gardener.cloud")) @@ -89,26 +80,94 @@ var _ = Describe("Reconcile", func() { Expect(role.Rules[1].Resources).To(ContainElements("secrets", "configmaps")) Expect(role.Rules[1].Verbs).To(ContainElement("*")) - //check for rolebinding being created + // check for rolebinding being created rolebinding := rbacv1.RoleBinding{} Expect(testenv.Client.Get(ctx, types.NamespacedName{Name: subjectsync.USER_ROLE_BINDING_IN_NAMESPACE, Namespace: namespace.Name}, &rolebinding)).To(Succeed()) Expect(rolebinding.RoleRef.Name).To(Equal(subjectsync.USER_ROLE_IN_NAMESPACE)) Expect(len(rolebinding.Subjects)).To(Equal(1)) Expect(rolebinding.Subjects[0].Kind).To(Equal("User")) Expect(rolebinding.Subjects[0].Name).To(Equal("testuser")) + + // delete and reconcile + Expect(testenv.Client.Delete(ctx, namespaceRegistration)).To(Succeed()) + testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) + + // check successful deletion + // (Since the namespace contains no installations etc., the deletion and reconciliation + // should have the effect that namespace and namespace registration disappear.) + Expect(testenv.WaitForObjectToBeDeleted(ctx, testenv.Client, namespaceRegistration, 5*time.Second)).To(Succeed()) + Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(&namespace), &namespace)).To(Succeed()) + Expect(namespace.Status.Phase).To(Equal(corev1.NamespaceTerminating)) }) - It("should delete namespace with role/rolebinding on namespaceregistration deletion", func() { + It("should delete namespace with installation", func() { var err error - state, err = testenv.InitResources(ctx, "./testdata/reconcile/test2") + state, err = testenv.InitResources(ctx, "./testdata/reconcile/test3") Expect(err).ToNot(HaveOccurred()) - namespaceRegistration := state.GetNamespaceRegistration(subjectsync.CUSTOM_NS_PREFIX + "test-namespace-2") - //reconcile for finalizer + namespaceRegistration := state.GetNamespaceRegistration(subjectsync.CUSTOM_NS_PREFIX + "test-namespace-3") + + // reconcile testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(namespaceRegistration), namespaceRegistration)).To(Succeed()) - //reconcile for actual run + Expect(namespaceRegistration.Status.Phase).To(Equal("Completed")) + + // check for namespace being created + namespace := corev1.Namespace{} + Expect(testenv.Client.Get(ctx, types.NamespacedName{Name: namespaceRegistration.Name}, &namespace)).To(Succeed()) + + // add Installation to prevent delete + compRef := &lsv1alpha1.ComponentDescriptorDefinition{ + Reference: &lsv1alpha1.ComponentDescriptorReference{ + ComponentName: "component", + Version: "v0.1.0", + }, + } + + installation := &lsv1alpha1.Installation{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{Name: namespace.Name, Namespace: namespace.Name}, + Spec: lsv1alpha1.InstallationSpec{ + ComponentDescriptor: compRef, + }, + } + + Expect(testenv.Client.Create(ctx, installation)).To(Succeed()) + + // delete + Expect(testenv.Client.Delete(ctx, namespaceRegistration)).To(Succeed()) + testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) + + // failed deletion + Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(installation), installation)).To(Succeed()) + Expect(installation.DeletionTimestamp.IsZero()).To(BeTrue()) + Expect(helper.HasDeleteWithoutUninstallAnnotation(installation.ObjectMeta)).To(BeFalse()) + Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(namespaceRegistration), namespaceRegistration)).To(Succeed()) + Expect(namespaceRegistration.Status.Phase).To(Equal("Deleting")) + Expect(testenv.Client.Get(ctx, types.NamespacedName{Name: namespaceRegistration.Name}, &namespace)).To(Succeed()) + Expect(namespace.DeletionTimestamp.IsZero()).To(BeTrue()) + + // successful deletion + Expect(testenv.Client.Delete(ctx, installation)).To(Succeed()) + Expect(testenv.WaitForObjectToBeDeleted(ctx, testenv.Client, installation, 5*time.Second)).To(Succeed()) + + testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) + Expect(testenv.WaitForObjectToBeDeleted(ctx, testenv.Client, namespaceRegistration, 5*time.Second)).To(Succeed()) + Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(&namespace), &namespace)).To(Succeed()) + + // check for namespace being deleted + Expect(namespace.Status.Phase).To(Equal(corev1.NamespaceTerminating)) + }) + + It("should delete namespace with execution", func() { + var err error + + state, err = testenv.InitResources(ctx, "./testdata/reconcile/test4") + Expect(err).ToNot(HaveOccurred()) + + namespaceRegistration := state.GetNamespaceRegistration(subjectsync.CUSTOM_NS_PREFIX + "test-namespace-4") + //reconcile testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(namespaceRegistration), namespaceRegistration)).To(Succeed()) Expect(namespaceRegistration.Status.Phase).To(Equal("Completed")) @@ -117,25 +176,295 @@ var _ = Describe("Reconcile", func() { namespace := corev1.Namespace{} Expect(testenv.Client.Get(ctx, types.NamespacedName{Name: namespaceRegistration.Name}, &namespace)).To(Succeed()) - //check for role being created - role := rbacv1.Role{} - Expect(testenv.Client.Get(ctx, types.NamespacedName{Name: subjectsync.USER_ROLE_IN_NAMESPACE, Namespace: namespace.Name}, &role)).To(Succeed()) + // add execution to prevent delete + execution := &lsv1alpha1.Execution{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{Name: namespace.Name, Namespace: namespace.Name}, + } - //check for rolebinding being created - rolebinding := rbacv1.RoleBinding{} - Expect(testenv.Client.Get(ctx, types.NamespacedName{Name: subjectsync.USER_ROLE_BINDING_IN_NAMESPACE, Namespace: namespace.Name}, &rolebinding)).To(Succeed()) - Expect(rolebinding.RoleRef.Name).To(Equal(subjectsync.USER_ROLE_IN_NAMESPACE)) - Expect(len(rolebinding.Subjects)).To(Equal(1)) - Expect(rolebinding.Subjects[0].Kind).To(Equal("User")) - Expect(rolebinding.Subjects[0].Name).To(Equal("testuser")) + Expect(testenv.Client.Create(ctx, execution)).To(Succeed()) + + // delete + Expect(testenv.Client.Delete(ctx, namespaceRegistration)).To(Succeed()) + testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) + + // failed deletion + Expect(testenv.Client.Get(ctx, types.NamespacedName{Name: namespaceRegistration.Name}, &namespace)).To(Succeed()) + Expect(namespace.DeletionTimestamp.IsZero()).To(BeTrue()) + + // successful deletion + Expect(testenv.Client.Delete(ctx, execution)).To(Succeed()) + Expect(testenv.WaitForObjectToBeDeleted(ctx, testenv.Client, execution, 5*time.Second)).To(Succeed()) + testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) + Expect(testenv.WaitForObjectToBeDeleted(ctx, testenv.Client, namespaceRegistration, 5*time.Second)).To(Succeed()) + Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(&namespace), &namespace)).To(Succeed()) + + // check for namespace being deleted + Expect(namespace.Status.Phase).To(Equal(corev1.NamespaceTerminating)) + }) + + It("should delete namespace with deploy item", func() { + var err error + + state, err = testenv.InitResources(ctx, "./testdata/reconcile/test5") + Expect(err).ToNot(HaveOccurred()) + + namespaceRegistration := state.GetNamespaceRegistration(subjectsync.CUSTOM_NS_PREFIX + "test-namespace-5") + //reconcile + testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) + Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(namespaceRegistration), namespaceRegistration)).To(Succeed()) + Expect(namespaceRegistration.Status.Phase).To(Equal("Completed")) - // deletion + // check for namespace being created + namespace := corev1.Namespace{} + Expect(testenv.Client.Get(ctx, types.NamespacedName{Name: namespaceRegistration.Name}, &namespace)).To(Succeed()) + + // add execution to prevent delete + di := &lsv1alpha1.DeployItem{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{Name: namespace.Name, Namespace: namespace.Name}, + } + + Expect(testenv.Client.Create(ctx, di)).To(Succeed()) + + // delete Expect(testenv.Client.Delete(ctx, namespaceRegistration)).To(Succeed()) testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) + + // failed deletion + Expect(testenv.Client.Get(ctx, types.NamespacedName{Name: namespaceRegistration.Name}, &namespace)).To(Succeed()) + Expect(namespace.DeletionTimestamp.IsZero()).To(BeTrue()) + + // successful deletion + Expect(testenv.Client.Delete(ctx, di)).To(Succeed()) + Expect(testenv.WaitForObjectToBeDeleted(ctx, testenv.Client, di, 5*time.Second)).To(Succeed()) + testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) Expect(testenv.WaitForObjectToBeDeleted(ctx, testenv.Client, namespaceRegistration, 5*time.Second)).To(Succeed()) Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(&namespace), &namespace)).To(Succeed()) // check for namespace being deleted Expect(namespace.Status.Phase).To(Equal(corev1.NamespaceTerminating)) }) + + It("should delete namespace with target sync", func() { + var err error + + state, err = testenv.InitResources(ctx, "./testdata/reconcile/test6") + Expect(err).ToNot(HaveOccurred()) + + namespaceRegistration := state.GetNamespaceRegistration(subjectsync.CUSTOM_NS_PREFIX + "test-namespace-6") + //reconcile + testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) + Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(namespaceRegistration), namespaceRegistration)).To(Succeed()) + Expect(namespaceRegistration.Status.Phase).To(Equal("Completed")) + + // check for namespace being created + namespace := corev1.Namespace{} + Expect(testenv.Client.Get(ctx, types.NamespacedName{Name: namespaceRegistration.Name}, &namespace)).To(Succeed()) + + // add execution to prevent delete + targetSync := &lsv1alpha1.TargetSync{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{Name: namespace.Name, Namespace: namespace.Name}, + } + + Expect(testenv.Client.Create(ctx, targetSync)).To(Succeed()) + + targetSync = &lsv1alpha1.TargetSync{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{Name: namespace.Name + "-2", Namespace: namespace.Name}, + } + + Expect(testenv.Client.Create(ctx, targetSync)).To(Succeed()) + + // delete + Expect(testenv.Client.Delete(ctx, namespaceRegistration)).To(Succeed()) + + counter := 1 + result := testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) + for (result.Requeue || result.RequeueAfter > 0) && counter < 5 { + counter++ + result = testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) + time.Sleep(1 * time.Second) + } + + // successful deletion + Expect(testenv.WaitForObjectToBeDeleted(ctx, testenv.Client, namespaceRegistration, 5*time.Second)).To(Succeed()) + Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(&namespace), &namespace)).To(Succeed()) + + // check for namespace being deleted + Expect(namespace.Status.Phase).To(Equal(corev1.NamespaceTerminating)) + }) + + It("should delete namespace with installation without uninstall", func() { + var err error + + state, err = testenv.InitResources(ctx, "./testdata/reconcile/test7") + Expect(err).ToNot(HaveOccurred()) + + namespaceRegistration := state.GetNamespaceRegistration(subjectsync.CUSTOM_NS_PREFIX + "test-namespace-7") + //reconcile + testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) + Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(namespaceRegistration), namespaceRegistration)).To(Succeed()) + Expect(namespaceRegistration.Status.Phase).To(Equal("Completed")) + + // check for namespace being created + namespace := corev1.Namespace{} + Expect(testenv.Client.Get(ctx, types.NamespacedName{Name: namespaceRegistration.Name}, &namespace)).To(Succeed()) + + // add execution to prevent delete + inst := &lsv1alpha1.Installation{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{Name: namespace.Name, Namespace: namespace.Name}, + } + + metav1.SetMetaDataAnnotation(&inst.ObjectMeta, lsv1alpha1.DeleteWithoutUninstallAnnotation, "true") + controllerutil.AddFinalizer(inst, lsv1alpha1.LandscaperFinalizer) + Expect(testenv.Client.Create(ctx, inst)).To(Succeed()) + + // delete + Expect(testenv.Client.Delete(ctx, namespaceRegistration)).To(Succeed()) + + // delete installations + testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) + Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(inst), inst)).To(Succeed()) + Expect(inst.GetDeletionTimestamp().IsZero()).ToNot(BeTrue()) + Expect(helper.HasOperation(inst.ObjectMeta, lsv1alpha1.ReconcileOperation)).ToNot(BeTrue()) + + testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) + Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(inst), inst)).To(Succeed()) + Expect(helper.HasOperation(inst.ObjectMeta, lsv1alpha1.ReconcileOperation)).To(BeTrue()) + + controllerutil.RemoveFinalizer(inst, lsv1alpha1.LandscaperFinalizer) + Expect(testenv.Client.Update(ctx, inst)).To(Succeed()) + Expect(testenv.WaitForObjectToBeDeleted(ctx, testenv.Client, inst, 5*time.Second)).To(Succeed()) + testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) + + // successful deletion + Expect(testenv.WaitForObjectToBeDeleted(ctx, testenv.Client, namespaceRegistration, 5*time.Second)).To(Succeed()) + Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(&namespace), &namespace)).To(Succeed()) + + // check for namespace being deleted + Expect(namespace.Status.Phase).To(Equal(corev1.NamespaceTerminating)) + }) + + It("should delete a namespace registration with strategy delete-all-installations", func() { + var err error + + state, err = testenv.InitResources(ctx, "./testdata/reconcile/test8") + Expect(err).ToNot(HaveOccurred()) + + // reconcile namespace registration + namespaceRegistration := state.GetNamespaceRegistration(subjectsync.CUSTOM_NS_PREFIX + "test-namespace-8") + testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) + Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(namespaceRegistration), namespaceRegistration)).To(Succeed()) + Expect(namespaceRegistration.Status.Phase).To(Equal("Completed")) + + // check for customer namespace being created + namespace := corev1.Namespace{} + Expect(testenv.Client.Get(ctx, types.NamespacedName{Name: namespaceRegistration.Name}, &namespace)).To(Succeed()) + + // create installation in customer namespace + inst := &lsv1alpha1.Installation{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-installation", + Namespace: namespace.Name, + Finalizers: []string{lsv1alpha1.LandscaperFinalizer}, + }, + } + Expect(testenv.Client.Create(ctx, inst)).To(Succeed()) + + // delete namespace registration + Expect(testenv.Client.Delete(ctx, namespaceRegistration)).To(Succeed()) + + // reconcile namespace registration + testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) + + // check namespace registration is in phase "Deleting" + Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(namespaceRegistration), namespaceRegistration)).To(Succeed()) + Expect(namespaceRegistration.Status.Phase).To(Equal("Deleting")) + + // check installation has deletion timestamp + Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(inst), inst)).To(Succeed()) + Expect(inst.GetDeletionTimestamp().IsZero()).To(BeFalse()) + Expect(helper.HasOperation(inst.ObjectMeta, lsv1alpha1.ReconcileOperation)).To(BeFalse()) + + // check installation does not have delete-without-uninstall annotation + // (difference to the next test) + Expect(helper.HasDeleteWithoutUninstallAnnotation(inst.ObjectMeta)).To(BeFalse()) + + // remove finalizer from installation, and wait until the installation is gone + controllerutil.RemoveFinalizer(inst, lsv1alpha1.LandscaperFinalizer) + Expect(testenv.Client.Update(ctx, inst)).To(Succeed()) + Expect(testenv.WaitForObjectToBeDeleted(ctx, testenv.Client, inst, 5*time.Second)).To(Succeed()) + + // reconcile namespace registration + testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) + + // check successful deletion + Expect(testenv.WaitForObjectToBeDeleted(ctx, testenv.Client, namespaceRegistration, 5*time.Second)).To(Succeed()) + Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(&namespace), &namespace)).To(Succeed()) + Expect(namespace.Status.Phase).To(Equal(corev1.NamespaceTerminating)) + }) + + It("should delete a namespace registration with strategy delete-all-installations-without-uninstall", func() { + var err error + + state, err = testenv.InitResources(ctx, "./testdata/reconcile/test9") + Expect(err).ToNot(HaveOccurred()) + + // reconcile namespace registration + namespaceRegistration := state.GetNamespaceRegistration(subjectsync.CUSTOM_NS_PREFIX + "test-namespace-9") + testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) + Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(namespaceRegistration), namespaceRegistration)).To(Succeed()) + Expect(namespaceRegistration.Status.Phase).To(Equal("Completed")) + + // check for customer namespace being created + namespace := corev1.Namespace{} + Expect(testenv.Client.Get(ctx, types.NamespacedName{Name: namespaceRegistration.Name}, &namespace)).To(Succeed()) + + // create installation in customer namespace + inst := &lsv1alpha1.Installation{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-installation", + Namespace: namespace.Name, + Finalizers: []string{lsv1alpha1.LandscaperFinalizer}, + }, + } + Expect(testenv.Client.Create(ctx, inst)).To(Succeed()) + + // delete namespace registration + Expect(testenv.Client.Delete(ctx, namespaceRegistration)).To(Succeed()) + + // reconcile namespace registration + testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) + + // check namespace registration is in phase "Deleting" + Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(namespaceRegistration), namespaceRegistration)).To(Succeed()) + Expect(namespaceRegistration.Status.Phase).To(Equal("Deleting")) + + // check installation has deletion timestamp + Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(inst), inst)).To(Succeed()) + Expect(inst.GetDeletionTimestamp().IsZero()).To(BeFalse()) + Expect(helper.HasOperation(inst.ObjectMeta, lsv1alpha1.ReconcileOperation)).To(BeFalse()) + + // check installation has delete-without-uninstall annotation + // (difference to the previous test) + Expect(helper.HasDeleteWithoutUninstallAnnotation(inst.ObjectMeta)).To(BeTrue()) + + // remove finalizer from installation, and wait until the installation is gone + controllerutil.RemoveFinalizer(inst, lsv1alpha1.LandscaperFinalizer) + Expect(testenv.Client.Update(ctx, inst)).To(Succeed()) + Expect(testenv.WaitForObjectToBeDeleted(ctx, testenv.Client, inst, 5*time.Second)).To(Succeed()) + + // reconcile namespace registration + testutils.ShouldReconcile(ctx, ctrl, testutils.RequestFromObject(namespaceRegistration)) + + // check successful deletion + Expect(testenv.WaitForObjectToBeDeleted(ctx, testenv.Client, namespaceRegistration, 5*time.Second)).To(Succeed()) + Expect(testenv.Client.Get(ctx, kutil.ObjectKeyFromObject(&namespace), &namespace)).To(Succeed()) + Expect(namespace.Status.Phase).To(Equal(corev1.NamespaceTerminating)) + }) }) diff --git a/pkg/controllers/namespaceregistration/testdata/reconcile/test2/namespaceregistration.yaml b/pkg/controllers/namespaceregistration/testdata/reconcile/test3/namespaceregistration.yaml similarity index 81% rename from pkg/controllers/namespaceregistration/testdata/reconcile/test2/namespaceregistration.yaml rename to pkg/controllers/namespaceregistration/testdata/reconcile/test3/namespaceregistration.yaml index 713b17774..4b634dc08 100644 --- a/pkg/controllers/namespaceregistration/testdata/reconcile/test2/namespaceregistration.yaml +++ b/pkg/controllers/namespaceregistration/testdata/reconcile/test3/namespaceregistration.yaml @@ -1,5 +1,5 @@ apiVersion: landscaper-service.gardener.cloud/v1alpha1 kind: NamespaceRegistration metadata: - name: cu-test-namespace-2 + name: cu-test-namespace-3 namespace: {{ .Namespace }} diff --git a/pkg/controllers/namespaceregistration/testdata/reconcile/test4/namespaceregistration.yaml b/pkg/controllers/namespaceregistration/testdata/reconcile/test4/namespaceregistration.yaml new file mode 100644 index 000000000..208fb7132 --- /dev/null +++ b/pkg/controllers/namespaceregistration/testdata/reconcile/test4/namespaceregistration.yaml @@ -0,0 +1,5 @@ +apiVersion: landscaper-service.gardener.cloud/v1alpha1 +kind: NamespaceRegistration +metadata: + name: cu-test-namespace-4 + namespace: {{ .Namespace }} diff --git a/pkg/controllers/namespaceregistration/testdata/reconcile/test5/namespaceregistration.yaml b/pkg/controllers/namespaceregistration/testdata/reconcile/test5/namespaceregistration.yaml new file mode 100644 index 000000000..10af49955 --- /dev/null +++ b/pkg/controllers/namespaceregistration/testdata/reconcile/test5/namespaceregistration.yaml @@ -0,0 +1,5 @@ +apiVersion: landscaper-service.gardener.cloud/v1alpha1 +kind: NamespaceRegistration +metadata: + name: cu-test-namespace-5 + namespace: {{ .Namespace }} diff --git a/pkg/controllers/namespaceregistration/testdata/reconcile/test6/namespaceregistration.yaml b/pkg/controllers/namespaceregistration/testdata/reconcile/test6/namespaceregistration.yaml new file mode 100644 index 000000000..b4c447c19 --- /dev/null +++ b/pkg/controllers/namespaceregistration/testdata/reconcile/test6/namespaceregistration.yaml @@ -0,0 +1,5 @@ +apiVersion: landscaper-service.gardener.cloud/v1alpha1 +kind: NamespaceRegistration +metadata: + name: cu-test-namespace-6 + namespace: {{ .Namespace }} diff --git a/pkg/controllers/namespaceregistration/testdata/reconcile/test7/namespaceregistration.yaml b/pkg/controllers/namespaceregistration/testdata/reconcile/test7/namespaceregistration.yaml new file mode 100644 index 000000000..66f1eb9e5 --- /dev/null +++ b/pkg/controllers/namespaceregistration/testdata/reconcile/test7/namespaceregistration.yaml @@ -0,0 +1,5 @@ +apiVersion: landscaper-service.gardener.cloud/v1alpha1 +kind: NamespaceRegistration +metadata: + name: cu-test-namespace-7 + namespace: {{ .Namespace }} diff --git a/pkg/controllers/namespaceregistration/testdata/reconcile/test8/namespaceregistration.yaml b/pkg/controllers/namespaceregistration/testdata/reconcile/test8/namespaceregistration.yaml new file mode 100644 index 000000000..abff0ebcf --- /dev/null +++ b/pkg/controllers/namespaceregistration/testdata/reconcile/test8/namespaceregistration.yaml @@ -0,0 +1,7 @@ +apiVersion: landscaper-service.gardener.cloud/v1alpha1 +kind: NamespaceRegistration +metadata: + name: cu-test-namespace-8 + namespace: {{ .Namespace }} + annotations: + landscaper-service.gardener.cloud/on-delete-strategy: delete-all-installations diff --git a/pkg/controllers/namespaceregistration/testdata/reconcile/test9/namespaceregistration.yaml b/pkg/controllers/namespaceregistration/testdata/reconcile/test9/namespaceregistration.yaml new file mode 100644 index 000000000..eb95cc10a --- /dev/null +++ b/pkg/controllers/namespaceregistration/testdata/reconcile/test9/namespaceregistration.yaml @@ -0,0 +1,7 @@ +apiVersion: landscaper-service.gardener.cloud/v1alpha1 +kind: NamespaceRegistration +metadata: + name: cu-test-namespace-9 + namespace: {{ .Namespace }} + annotations: + landscaper-service.gardener.cloud/on-delete-strategy: delete-all-installations-without-uninstall diff --git a/pkg/controllers/subjectsync/add.go b/pkg/controllers/subjectsync/add.go index f4038d220..a1d49f833 100644 --- a/pkg/controllers/subjectsync/add.go +++ b/pkg/controllers/subjectsync/add.go @@ -16,7 +16,7 @@ import ( "github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1" ) -// AddControllerToManager adds the Namespaceregistration Controller to the manager +// AddControllerToManager adds the SubjectList Controller to the manager func AddControllerToManager(logger logging.Logger, mgr manager.Manager, config *coreconfig.TargetShootSidecarConfiguration) error { log := logger.Reconciles("SubjectSyncController", "SubjectList") ctrl, err := NewController(log, mgr.GetClient(), mgr.GetScheme(), config) diff --git a/pkg/controllers/subjectsync/controller.go b/pkg/controllers/subjectsync/controller.go index 531e1f08d..b97f53e2b 100644 --- a/pkg/controllers/subjectsync/controller.go +++ b/pkg/controllers/subjectsync/controller.go @@ -54,6 +54,8 @@ func NewTestActuator(op operation.TargetShootSidecarOperation, logger logging.Lo func (c *Controller) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { logger, ctx := c.log.StartReconcileAndAddToContext(ctx, req) + logger.Info("start reconcile subjectList") + subjectList := &lssv1alpha1.SubjectList{} if err := c.Client().Get(ctx, req.NamespacedName, subjectList); err != nil { logger.Error(err, "failed loading subjectlist cr") @@ -69,7 +71,7 @@ func (c *Controller) Reconcile(ctx context.Context, req reconcile.Request) (reco if err := c.Client().Update(ctx, subjectList); err != nil { return reconcile.Result{}, err } - return reconcile.Result{}, nil + return reconcile.Result{Requeue: true}, nil } if !subjectList.DeletionTimestamp.IsZero() { diff --git a/pkg/crdmanager/crdresources/landscaper-service.gardener.cloud_namespaceregistrations.yaml b/pkg/crdmanager/crdresources/landscaper-service.gardener.cloud_namespaceregistrations.yaml index 8b9f846d0..7f68a8225 100755 --- a/pkg/crdmanager/crdresources/landscaper-service.gardener.cloud_namespaceregistrations.yaml +++ b/pkg/crdmanager/crdresources/landscaper-service.gardener.cloud_namespaceregistrations.yaml @@ -27,6 +27,36 @@ spec: status: description: Status contains the status for the NamespaceRegistration. properties: + lastError: + description: Error holds information about an error that occurred. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + lastUpdateTime: + description: Last time the condition was updated. + format: date-time + type: string + message: + description: A human-readable message indicating details about + the transition. + type: string + operation: + description: Operation describes the operator where the error + occurred. + type: string + reason: + description: The reason for the condition's last transition. + type: string + required: + - operation + - lastTransitionTime + - lastUpdateTime + - reason + - message + type: object phase: type: string required: diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 2727a89c2..cbb823782 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -8,6 +8,8 @@ import ( "fmt" "strconv" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" lsv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" @@ -84,3 +86,21 @@ func RemoveOperationAnnotation(object client.Object) { delete(annotations, lssv1alpha1.LandscaperServiceOperationAnnotation) } } + +// HasLabel checks if the objects has a label +func HasLabel(obj metav1.Object, lab string) bool { + labels := obj.GetLabels() + if labels == nil { + return false + } + _, ok := labels[lab] + return ok +} + +// HasDeleteWithoutUninstallAnnotation returns true only if the given object +// has the 'landscaper.gardener.cloud/delete-without-uninstall' annotation +// and its value is 'true'. +func HasDeleteWithoutUninstallAnnotation(obj metav1.Object) bool { + v, ok := obj.GetAnnotations()[lsv1alpha1.DeleteWithoutUninstallAnnotation] + return ok && v == "true" +} diff --git a/test/utils/controller.go b/test/utils/controller.go index 1f95558c4..fb3a102de 100644 --- a/test/utils/controller.go +++ b/test/utils/controller.go @@ -36,9 +36,10 @@ func RequestFromObject(obj client.Object) reconcile.Request { // ShouldReconcile reconciles the given reconciler with the given request // and expects that no error occurred -func ShouldReconcile(ctx context.Context, reconciler reconcile.Reconciler, req reconcile.Request, optionalDescription ...interface{}) { - _, err := reconciler.Reconcile(ctx, req) +func ShouldReconcile(ctx context.Context, reconciler reconcile.Reconciler, req reconcile.Request, optionalDescription ...interface{}) reconcile.Result { + res, err := reconciler.Reconcile(ctx, req) gomega.ExpectWithOffset(1, err).ToNot(gomega.HaveOccurred(), optionalDescription...) + return res } // ShouldNotReconcile reconciles the given reconciler with the given request diff --git a/test/utils/envtest/environment.go b/test/utils/envtest/environment.go index 9b6f8fd8e..480c17946 100644 --- a/test/utils/envtest/environment.go +++ b/test/utils/envtest/environment.go @@ -393,19 +393,37 @@ func (e *Environment) decodeAndAppendLSSObject(data []byte, objects []client.Obj case ConfigMapGVK.Kind: configMap := &corev1.ConfigMap{} if _, _, err := decoder.Decode(data, nil, configMap); err != nil { - return nil, fmt.Errorf("unable to decode file as secret: %w", err) + return nil, fmt.Errorf("unable to decode file as config map: %w", err) } return append(objects, configMap), nil case InstallationGVK.Kind: installation := &lsv1alpha1.Installation{} if _, _, err := decoder.Decode(data, nil, installation); err != nil { - return nil, fmt.Errorf("unable to decode file as secret: %w", err) + return nil, fmt.Errorf("unable to decode file as installation: %w", err) } return append(objects, installation), nil + case ExecutionGVK.Kind: + execution := &lsv1alpha1.Execution{} + if _, _, err := decoder.Decode(data, nil, execution); err != nil { + return nil, fmt.Errorf("unable to decode file as execution: %w", err) + } + return append(objects, execution), nil + case DeployItemGVK.Kind: + di := &lsv1alpha1.DeployItem{} + if _, _, err := decoder.Decode(data, nil, di); err != nil { + return nil, fmt.Errorf("unable to decode file as deploy item: %w", err) + } + return append(objects, di), nil + case TargetSyncGVK.Kind: + di := &lsv1alpha1.TargetSync{} + if _, _, err := decoder.Decode(data, nil, di); err != nil { + return nil, fmt.Errorf("unable to decode file as targetsync: %w", err) + } + return append(objects, di), nil case TargetGVK.Kind: target := &lsv1alpha1.Target{} if _, _, err := decoder.Decode(data, nil, target); err != nil { - return nil, fmt.Errorf("unable to decode file as secret: %w", err) + return nil, fmt.Errorf("unable to decode file as target: %w", err) } return append(objects, target), nil case ContextGVK.Kind: diff --git a/test/utils/envtest/state.go b/test/utils/envtest/state.go index 0afcc71e0..c6c4d31c3 100644 --- a/test/utils/envtest/state.go +++ b/test/utils/envtest/state.go @@ -32,6 +32,12 @@ type State struct { ConfigMaps map[string]*corev1.ConfigMap // Installations contains all installations in this test environment. Installations map[string]*lsv1alpha1.Installation + // Executions contains all executions in this test environment. + Executions map[string]*lsv1alpha1.Execution + // DeployItems contains all DeployItems in this test environment. + DeployItems map[string]*lsv1alpha1.DeployItem + // TargetSync contains all targetsyncs in this test environment. + TargetSync map[string]*lsv1alpha1.TargetSync // Targets contains all targets in this test environment. Targets map[string]*lsv1alpha1.Target // Contexts contains all contexts in this test environment @@ -56,6 +62,9 @@ func NewState(namespace string) *State { Secrets: make(map[string]*corev1.Secret), ConfigMaps: make(map[string]*corev1.ConfigMap), Installations: make(map[string]*lsv1alpha1.Installation), + Executions: make(map[string]*lsv1alpha1.Execution), + DeployItems: make(map[string]*lsv1alpha1.DeployItem), + TargetSync: make(map[string]*lsv1alpha1.TargetSync), Targets: make(map[string]*lsv1alpha1.Target), Contexts: make(map[string]*lsv1alpha1.Context), AvailabilityCollections: make(map[string]*lssv1alpha1.AvailabilityCollection), @@ -95,6 +104,18 @@ func (s *State) GetInstallation(name string) *lsv1alpha1.Installation { return s.Installations[s.Namespace+"/"+name] } +func (s *State) GetExecution(name string) *lsv1alpha1.Execution { + return s.Executions[s.Namespace+"/"+name] +} + +func (s *State) GetDeployItem(name string) *lsv1alpha1.DeployItem { + return s.DeployItems[s.Namespace+"/"+name] +} + +func (s *State) GetTargetSync(name string) *lsv1alpha1.TargetSync { + return s.TargetSync[s.Namespace+"/"+name] +} + // GetTarget retrieves a target by the given name. func (s *State) GetTarget(name string) *lsv1alpha1.Target { return s.Targets[s.Namespace+"/"+name] @@ -151,6 +172,12 @@ func (s *State) AddObject(object client.Object) { s.ConfigMaps[types.NamespacedName{Name: o.Name, Namespace: o.Namespace}.String()] = o.DeepCopy() case *lsv1alpha1.Installation: s.Installations[types.NamespacedName{Name: o.Name, Namespace: o.Namespace}.String()] = o.DeepCopy() + case *lsv1alpha1.Execution: + s.Executions[types.NamespacedName{Name: o.Name, Namespace: o.Namespace}.String()] = o.DeepCopy() + case *lsv1alpha1.DeployItem: + s.DeployItems[types.NamespacedName{Name: o.Name, Namespace: o.Namespace}.String()] = o.DeepCopy() + case *lsv1alpha1.TargetSync: + s.TargetSync[types.NamespacedName{Name: o.Name, Namespace: o.Namespace}.String()] = o.DeepCopy() case *lsv1alpha1.Target: s.Targets[types.NamespacedName{Name: o.Name, Namespace: o.Namespace}.String()] = o.DeepCopy() case *lsv1alpha1.Context: diff --git a/test/utils/envtest/types.go b/test/utils/envtest/types.go index fdfa622ba..45f5fa943 100644 --- a/test/utils/envtest/types.go +++ b/test/utils/envtest/types.go @@ -34,6 +34,12 @@ var ( ConfigMapGVK schema.GroupVersionKind // InstallationGVK is the GVK for installations. InstallationGVK schema.GroupVersionKind + // ExecutionGVK is the GVK for executions. + ExecutionGVK schema.GroupVersionKind + // DeployItemGVK is the GVK for deploy items. + DeployItemGVK schema.GroupVersionKind + // TargetSyncGVK is the GVK for target syncs. + TargetSyncGVK schema.GroupVersionKind // TargetGVK is the GVK for targets. TargetGVK schema.GroupVersionKind // ContextGVK is the GVK for contexts. @@ -67,6 +73,12 @@ func init() { utilruntime.Must(err) InstallationGVK, err = apiutil.GVKForObject(&lsv1alpha1.Installation{}, LandscaperServiceScheme) utilruntime.Must(err) + ExecutionGVK, err = apiutil.GVKForObject(&lsv1alpha1.Execution{}, LandscaperServiceScheme) + utilruntime.Must(err) + DeployItemGVK, err = apiutil.GVKForObject(&lsv1alpha1.DeployItem{}, LandscaperServiceScheme) + utilruntime.Must(err) + TargetSyncGVK, err = apiutil.GVKForObject(&lsv1alpha1.TargetSync{}, LandscaperServiceScheme) + utilruntime.Must(err) TargetGVK, err = apiutil.GVKForObject(&lsv1alpha1.Target{}, LandscaperServiceScheme) utilruntime.Must(err) ContextGVK, err = apiutil.GVKForObject(&lsv1alpha1.Context{}, LandscaperServiceScheme) diff --git a/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/helper/dataobjects.go b/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/helper/dataobjects.go new file mode 100644 index 000000000..b38498a41 --- /dev/null +++ b/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/helper/dataobjects.go @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: 2020 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package helper + +import ( + "crypto/sha1" + "encoding/base32" + "fmt" + "regexp" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + lsv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" +) + +const Base32EncodeStdLowerCase = "abcdefghijklmnopqrstuvwxyz234567" + +const SourceDelimiter = "/" + +const NonContextifiedPrefix = "#" + +// InstallationPrefix is the prefix combined with installation name is used as label value. Do not change length. +const InstallationPrefix = "Inst." + +// ExecutionPrefix is the prefix combined with execution name is used as label value. Do not change length. +const ExecutionPrefix = "Exec." + +var subdomainRegex = regexp.MustCompile("^([a-z0-9]|([a-z0-9][a-z0-9.-]*[a-z0-9]))$") + +// GenerateDataObjectName generates the unique name for a data object exported or imported by a installation. +// It returns a non contextified data name if the name starts with a "#". +func GenerateDataObjectName(context string, name string) string { + if strings.HasPrefix(name, NonContextifiedPrefix) { + return strings.TrimPrefix(name, NonContextifiedPrefix) + } + // for backward compatibility, we need to hash names which are incompatible with the k8s resource naming scheme + if len(context) == 0 && subdomainRegex.MatchString(name) { + return name + } + doName := fmt.Sprintf("%s/%s", context, name) + h := sha1.New() + _, _ = h.Write([]byte(doName)) + // we need base32 encoding as some base64 (even url safe base64) characters are not supported by k8s + // see https://kubernetes.io/docs/concepts/overview/working-with-objects/names/ + return base32.NewEncoding(Base32EncodeStdLowerCase).WithPadding(base32.NoPadding).EncodeToString(h.Sum(nil)) +} + +// GenerateDataObjectNameWithIndex generates a unique name for a data object which is part of a list +// and therefore has no own name but is identified by a combination of name and index. +// It builds a fake name by combining name and index and then calls GenerateDataObjectName. +func GenerateDataObjectNameWithIndex(context string, name string, index int) string { + return GenerateDataObjectName(context, fmt.Sprintf("%s[%d]", name, index)) +} + +// DataObjectSourceFromObject returns the data object source for a runtime object. +func DataObjectSourceFromObject(src runtime.Object) (string, error) { + acc, ok := src.(metav1.Object) + if !ok { + return "", fmt.Errorf("source has to be a kubernetes metadata object") + } + + srcKind := src.GetObjectKind().GroupVersionKind().Kind + return srcKind + SourceDelimiter + acc.GetNamespace() + SourceDelimiter + acc.GetName(), nil +} + +// ObjectFromDataObjectSource parses the source's kind, namespace and name from a src string. +func ObjectFromDataObjectSource(src string) (string, lsv1alpha1.ObjectReference, error) { + splitValues := strings.Split(src, SourceDelimiter) + if len(splitValues) != 3 { + return "", lsv1alpha1.ObjectReference{}, fmt.Errorf("expected source definition with 3 paramters but got %d", len(splitValues)) + } + + kind, namespace, name := splitValues[0], splitValues[1], splitValues[2] + return kind, lsv1alpha1.ObjectReference{Namespace: namespace, Name: name}, nil +} + +// DataObjectSourceFromInstallation returns the data object source for a Installation. +func DataObjectSourceFromInstallation(src *lsv1alpha1.Installation) string { + return InstallationPrefix + src.GetName() +} + +// DataObjectSourceFromInstallationName returns the data object source for an Installation name. +func DataObjectSourceFromInstallationName(name string) string { + return InstallationPrefix + name +} + +// DataObjectSourceFromExecution returns the data object source for a Execution. +func DataObjectSourceFromExecution(src *lsv1alpha1.Execution) string { + return ExecutionPrefix + src.GetName() +} diff --git a/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/helper/helpers.go b/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/helper/helpers.go new file mode 100644 index 000000000..1e6626a73 --- /dev/null +++ b/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/helper/helpers.go @@ -0,0 +1,254 @@ +// SPDX-FileCopyrightText: 2020 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package helper + +import ( + "reflect" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/gardener/landscaper/apis/core/v1alpha1" +) + +type TimestampAnnotation string + +const ( + ReconcileTimestamp = TimestampAnnotation(v1alpha1.ReconcileTimestampAnnotation) +) + +// HasOperation checks if the obj has the given operation annotation +func HasOperation(obj metav1.ObjectMeta, op v1alpha1.Operation) bool { + currentOp, ok := obj.Annotations[v1alpha1.OperationAnnotation] + if !ok { + return false + } + + return v1alpha1.Operation(currentOp) == op +} + +func GetOperation(obj metav1.ObjectMeta) string { + if obj.Annotations == nil { + return "" + } + return obj.Annotations[v1alpha1.OperationAnnotation] +} + +// SetOperation sets the given operation annotation on aa object. +func SetOperation(obj *metav1.ObjectMeta, op v1alpha1.Operation) { + metav1.SetMetaDataAnnotation(obj, v1alpha1.OperationAnnotation, string(op)) +} + +func GetTimestampAnnotation(obj metav1.ObjectMeta, ta TimestampAnnotation) (time.Time, error) { + return time.Parse(time.RFC3339, obj.Annotations[string(ta)]) +} + +// SetTimestampAnnotationNow sets the timeout annotation with the current timestamp. +func SetTimestampAnnotationNow(obj *metav1.ObjectMeta, ta TimestampAnnotation) { + metav1.SetMetaDataAnnotation(obj, string(ta), time.Now().Format(time.RFC3339)) +} + +func Touch(obj *metav1.ObjectMeta) { + _, ok := obj.Annotations[v1alpha1.TouchAnnotation] + if ok { + delete(obj.Annotations, v1alpha1.TouchAnnotation) + } else { + metav1.SetMetaDataAnnotation(obj, v1alpha1.TouchAnnotation, "true") + } +} + +// InitCondition initializes a new Condition with an Unknown status. +func InitCondition(conditionType v1alpha1.ConditionType) v1alpha1.Condition { + return v1alpha1.Condition{ + Type: conditionType, + Status: v1alpha1.ConditionUnknown, + Reason: "ConditionInitialized", + Message: "The condition has been initialized but its semantic check has not been performed yet.", + LastTransitionTime: metav1.Now(), + } +} + +// GetCondition returns the condition with the given out of the list of . +// In case the required type could not be found, it returns nil. +func GetCondition(conditions []v1alpha1.Condition, conditionType v1alpha1.ConditionType) *v1alpha1.Condition { + for _, condition := range conditions { + if condition.Type == conditionType { + c := condition + return &c + } + } + return nil +} + +// GetOrInitCondition tries to retrieve the condition with the given condition type from the given conditions. +// If the condition could not be found, it returns an initialized condition of the given type. +func GetOrInitCondition(conditions []v1alpha1.Condition, conditionType v1alpha1.ConditionType) v1alpha1.Condition { + if condition := GetCondition(conditions, conditionType); condition != nil { + return *condition + } + return InitCondition(conditionType) +} + +// UpdatedCondition updates the properties of one specific condition. +func UpdatedCondition(condition v1alpha1.Condition, status v1alpha1.ConditionStatus, reason, message string, codes ...v1alpha1.ErrorCode) v1alpha1.Condition { + newCondition := v1alpha1.Condition{ + Type: condition.Type, + Status: status, + Reason: reason, + Message: message, + LastTransitionTime: condition.LastTransitionTime, + LastUpdateTime: condition.LastUpdateTime, + Codes: codes, + } + + if !reflect.DeepEqual(condition, newCondition) { + newCondition.LastUpdateTime = metav1.Now() + } + + if condition.Status != status { + newCondition.LastTransitionTime = metav1.Now() + } + + return newCondition +} + +// CreateOrUpdateConditions creates or updates a condition in a condition list. +func CreateOrUpdateConditions(conditions []v1alpha1.Condition, condType v1alpha1.ConditionType, status v1alpha1.ConditionStatus, reason, message string, codes ...v1alpha1.ErrorCode) []v1alpha1.Condition { + for i, foundCondition := range conditions { + if foundCondition.Type == condType { + conditions[i] = UpdatedCondition(conditions[i], status, reason, message, codes...) + return conditions + } + } + + return append(conditions, UpdatedCondition(InitCondition(condType), status, reason, message, codes...)) +} + +// MergeConditions merges the given with the . Existing conditions are superseded by +// the (depending on the condition type). +func MergeConditions(oldConditions []v1alpha1.Condition, newConditions ...v1alpha1.Condition) []v1alpha1.Condition { + var ( + out = make([]v1alpha1.Condition, 0, len(oldConditions)) + typeToIndex = make(map[v1alpha1.ConditionType]int, len(oldConditions)) + ) + + for i, condition := range oldConditions { + out = append(out, condition) + typeToIndex[condition.Type] = i + } + + for _, condition := range newConditions { + if index, ok := typeToIndex[condition.Type]; ok { + out[index] = condition + continue + } + out = append(out, condition) + } + + return out +} + +// IsConditionStatus returns if all condition states of all conditions are true. +func IsConditionStatus(conditions []v1alpha1.Condition, status v1alpha1.ConditionStatus) bool { + for _, condition := range conditions { + if condition.Status != status { + return false + } + } + return true +} + +// ObjectReferenceFromObject creates a object reference from a k8s object +func ObjectReferenceFromObject(obj metav1.Object) v1alpha1.ObjectReference { + return v1alpha1.ObjectReference{ + Namespace: obj.GetNamespace(), + Name: obj.GetName(), + } +} + +// CreateOrUpdateVersionedObjectReferences creates or updates a element in versioned objectReference slice. +func CreateOrUpdateVersionedObjectReferences(refs []v1alpha1.VersionedObjectReference, ref v1alpha1.ObjectReference, gen int64) []v1alpha1.VersionedObjectReference { + for i, vref := range refs { + if vref.ObjectReference == ref { + refs[i] = v1alpha1.VersionedObjectReference{ + ObjectReference: ref, + ObservedGeneration: gen, + } + return refs + } + } + return append(refs, v1alpha1.VersionedObjectReference{ + ObjectReference: ref, + ObservedGeneration: gen, + }) +} + +// GetNamedObjectReference returns the object reference with the given name. +func GetNamedObjectReference(objects []v1alpha1.NamedObjectReference, name string) (v1alpha1.NamedObjectReference, bool) { + for _, ref := range objects { + if ref.Name == name { + return ref, true + } + } + return v1alpha1.NamedObjectReference{}, false +} + +// ReferenceIsObject checks if the reference describes the given object. +func ReferenceIsObject(ref v1alpha1.ObjectReference, obj metav1.Object) bool { + return ref.Name == obj.GetName() && ref.Namespace == obj.GetNamespace() +} + +// SetVersionedNamedObjectReference sets the versioned object reference with the given name. +func SetVersionedNamedObjectReference(objects []v1alpha1.VersionedNamedObjectReference, obj v1alpha1.VersionedNamedObjectReference) []v1alpha1.VersionedNamedObjectReference { + for i, ref := range objects { + if ref.Name == obj.Name { + objects[i] = obj + return objects + } + } + return append(objects, obj) +} + +// RemoveVersionedNamedObjectReference removes the first versioned object reference with the given name. +func RemoveVersionedNamedObjectReference(objects []v1alpha1.VersionedNamedObjectReference, name string) []v1alpha1.VersionedNamedObjectReference { + for i, ref := range objects { + if ref.Name == name { + return append(objects[:i], objects[i+1:]...) + } + } + return objects +} + +// HasIgnoreAnnotation returns true only if the given object +// has the 'landscaper.gardener.cloud/ignore' annotation +// and its value is 'true'. +func HasIgnoreAnnotation(obj metav1.ObjectMeta) bool { + v, ok := obj.GetAnnotations()[v1alpha1.IgnoreAnnotation] + return ok && v == "true" +} + +// HasDeleteWithoutUninstallAnnotation returns true only if the given object +// has the 'landscaper.gardener.cloud/delete-without-uninstall' annotation +// and its value is 'true'. +func HasDeleteWithoutUninstallAnnotation(obj metav1.ObjectMeta) bool { + v, ok := obj.GetAnnotations()[v1alpha1.DeleteWithoutUninstallAnnotation] + return ok && v == "true" +} + +// SetDeployItemToFailed sets status.phase of the DeployItem to a failure phase +// If the DeployItem has a DeletionTimestamp, 'DeleteFailed' is used, otherwise it will be set to 'Failed'. +// Afterwards, the set phase is returned. +// Will do nothing and return an empty string if given a nil pointer. +func SetDeployItemToFailed(di *v1alpha1.DeployItem) v1alpha1.DeployItemPhase { + if di == nil { + return "" + } + if !di.ObjectMeta.DeletionTimestamp.IsZero() { + di.Status.Phase = v1alpha1.DeployItemPhases.DeleteFailed + } else { + di.Status.Phase = v1alpha1.DeployItemPhases.Failed + } + return di.Status.Phase +} diff --git a/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/helper/installationstate.go b/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/helper/installationstate.go new file mode 100644 index 000000000..1da327ae8 --- /dev/null +++ b/vendor/github.com/gardener/landscaper/apis/core/v1alpha1/helper/installationstate.go @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2020 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 + +package helper + +import landscaperv1alpha1 "github.com/gardener/landscaper/apis/core/v1alpha1" + +// NewInstallationReferenceState creates a new installation reference state from a given installation +func NewInstallationReferenceState(name string, inst *landscaperv1alpha1.Installation) landscaperv1alpha1.NamedObjectReference { + return landscaperv1alpha1.NamedObjectReference{ + Name: name, + Reference: landscaperv1alpha1.ObjectReference{ + Name: inst.Name, + Namespace: inst.Namespace, + }, + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index a8c50aada..211896710 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -42,6 +42,7 @@ github.com/gardener/landscaper/apis/config github.com/gardener/landscaper/apis/core github.com/gardener/landscaper/apis/core/install github.com/gardener/landscaper/apis/core/v1alpha1 +github.com/gardener/landscaper/apis/core/v1alpha1/helper github.com/gardener/landscaper/apis/core/v1alpha1/targettypes github.com/gardener/landscaper/apis/hack/generate-schemes/app github.com/gardener/landscaper/apis/hack/generate-schemes/generators