diff --git a/.github/actions/deploy-lifecycle-manager-e2e/action.yml b/.github/actions/deploy-lifecycle-manager-e2e/action.yml index c5a5535f50..38714fe8a6 100644 --- a/.github/actions/deploy-lifecycle-manager-e2e/action.yml +++ b/.github/actions/deploy-lifecycle-manager-e2e/action.yml @@ -109,7 +109,8 @@ runs: matrix.e2e-test == 'module-status-decoupling-with-deployment' || matrix.e2e-test == 'purge-metrics' || matrix.e2e-test == 'self-signed-certificate-rotation' || - matrix.e2e-test == 'mandatory-module-metrics'}} + matrix.e2e-test == 'mandatory-module-metrics' || + matrix.e2e-test == 'mandatory-module-metrics-with-old-naming-pattern'}} shell: bash run: | kubectl patch svc klm-controller-manager-metrics -p '{"spec": {"type": "LoadBalancer"}}' -n kcp-system diff --git a/.github/actions/deploy-template-operator-with-modulereleasemeta/action.yml b/.github/actions/deploy-template-operator-with-modulereleasemeta/action.yml index eeffd278f4..77ccaa2280 100644 --- a/.github/actions/deploy-template-operator-with-modulereleasemeta/action.yml +++ b/.github/actions/deploy-template-operator-with-modulereleasemeta/action.yml @@ -20,6 +20,9 @@ runs: cp ../lifecycle-manager/scripts/tests/deploy_modulereleasemeta.sh . - name: Create and apply Template Operator ModuleTemplate from the latest release working-directory: template-operator + if: ${{ matrix.e2e-test != 'mandatory-module' && + matrix.e2e-test != 'mandatory-module-metrics' + }} shell: bash run: | modulectl create --config-file ./module-config.yaml --registry http://localhost:5111 --insecure @@ -28,6 +31,9 @@ runs: kubectl apply -f template.yaml - name: Create and apply Template Operator ModuleTemplate with ModuleDeploymentNameInOlderVersion working-directory: template-operator + if: ${{ matrix.e2e-test != 'mandatory-module' && + matrix.e2e-test != 'mandatory-module-metrics' + }} shell: bash run: | make build-manifests @@ -35,6 +41,9 @@ runs: ./deploy_moduletemplate.sh ${{ env.ModuleName }} ${{ env.OlderVersion }} - name: Create and apply Template Operator ModuleTemplate with ModuleDeploymentNameInNewerVersion working-directory: template-operator + if: ${{ matrix.e2e-test != 'mandatory-module' && + matrix.e2e-test != 'mandatory-module-metrics' + }} shell: bash run: | make build-manifests @@ -70,14 +79,27 @@ runs: shell: bash run: | ./deploy_modulereleasemeta.sh ${{ env.ModuleName }} fast:${{ env.NewerVersion }} regular:${{ env.OlderVersion }} - - name: Create Template Operator Module as Mandatory Module - working-directory: lifecycle-manager + + - name: Create and apply Template Operator Module as Mandatory Module + working-directory: template-operator if: ${{ matrix.e2e-test == 'mandatory-module' || matrix.e2e-test == 'mandatory-module-metrics' }} shell: bash run: | - kubectl apply -f tests/e2e/moduletemplate/mandatory_moduletemplate_template_operator_v1.yaml + make build-manifests + yq eval '(. | select(.kind == "Deployment") | .metadata.name) = "${{ env.ModuleDeploymentNameInOlderVersion }}"' -i template-operator.yaml + ./deploy_moduletemplate.sh ${{ env.ModuleName }} ${{ env.OlderVersionForMandatoryModule }} true true + + - name: Create ModuleTemplate in a new version for Mandatory module + if: ${{ matrix.e2e-test == 'mandatory-module'}} + working-directory: template-operator + shell: bash + run: | + make build-manifests + yq eval '(. | select(.kind == "Deployment") | .metadata.name) = "${{ env.ModuleDeploymentNameInNewerVersion }}"' -i template-operator.yaml + ./deploy_moduletemplate.sh ${{ env.ModuleName }} ${{ env.NewerVersionForMandatoryModule }} true true false + cp template.yaml ../lifecycle-manager/tests/e2e/mandatory_template_v2.yaml - name: Create and apply ModuleReleaseMeta Template Operator with newer version in fast channel and older version in regular channel working-directory: template-operator if: ${{ matrix.e2e-test == 'non-blocking-deletion' }} diff --git a/.github/actions/deploy-template-operator/action.yml b/.github/actions/deploy-template-operator/action.yml index cc36ff2009..3f403d5d18 100644 --- a/.github/actions/deploy-template-operator/action.yml +++ b/.github/actions/deploy-template-operator/action.yml @@ -47,8 +47,8 @@ runs: kubectl apply -f tests/e2e/moduletemplate/moduletemplate_template_operator_v2_direct_version.yaml - name: Create Template Operator Module as Mandatory Module working-directory: lifecycle-manager - if: ${{ matrix.e2e-test == 'mandatory-module' || - matrix.e2e-test == 'mandatory-module-metrics' + if: ${{ matrix.e2e-test == 'mandatory-module-with-old-naming-pattern' || + matrix.e2e-test == 'mandatory-module-metrics-with-old-naming-pattern' }} shell: bash run: | diff --git a/.github/workflows/test-e2e-with-modulereleasemeta.yml b/.github/workflows/test-e2e-with-modulereleasemeta.yml index be91c3547c..520abf6d3e 100644 --- a/.github/workflows/test-e2e-with-modulereleasemeta.yml +++ b/.github/workflows/test-e2e-with-modulereleasemeta.yml @@ -115,6 +115,8 @@ jobs: ModuleDeploymentNameInOlderVersion: template-operator-v1-controller-manager NewerVersion: 2.4.2-e2e-test OlderVersion: 1.1.1-e2e-test + OlderVersionForMandatoryModule: 1.1.0-smoke-test + NewerVersionForMandatoryModule: 2.4.1-smoke-test VersionForStatefulSetInWarning: 1.0.0-warning-statefulset VersionForDeploymentInWarning: 1.0.0-warning-deployment VersionForMisconfiguredDeploymentImage: 1.0.0-misconfigured-deployment diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index abd6f28b17..7ccb95e697 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -58,8 +58,8 @@ jobs: - ca-certificate-rotation - istio-gateway-secret-rotation - self-signed-certificate-rotation - - mandatory-module - - mandatory-module-metrics + - mandatory-module-with-old-naming-pattern + - mandatory-module-metrics-with-old-naming-pattern - misconfigured-kyma-secret - rbac-privileges - ocm-compatible-module-template diff --git a/docs/contributor/02-controllers.md b/docs/contributor/02-controllers.md index e7467f261f..848af9bc9c 100644 --- a/docs/contributor/02-controllers.md +++ b/docs/contributor/02-controllers.md @@ -40,7 +40,7 @@ Lifecycle Manager uses two Mandatory Modules Controllers: * [Mandatory modules installation controller](../../internal/controller/mandatorymodule/installation_controller.go) deals with the reconciliation of mandatory modules * [Mandatory modules deletion controller](../../internal/controller/mandatorymodule/deletion_controller.go) deals with the deletion of mandatory modules -Since the channel concept does not apply to mandatory modules, the Mandatory Modules Installation Controller fetches all the Mandatory ModuleTemplate CRs without any channel filtering. It then translates the ModuleTemplate CR for the mandatory module to a [Manifest CR](../../api/v1beta2/manifest_types.go) with an OwnerReference to the Kyma CR. Similarly to the [Kyma Controller](../../internal/controller/kyma/controller.go), +Since the channel concept does not apply to mandatory modules, the Mandatory Modules Installation Controller fetches all the Mandatory ModuleTemplate CRs with the 'operator.kyma-project.io/mandatory-module' label. If multiple ModuleTemplates exist for the same mandatory module, the Controller fetches the ModuleTemplate with the highest version. It then translates the ModuleTemplate CR for the mandatory module to a [Manifest CR](../../api/v1beta2/manifest_types.go) with an OwnerReference to the Kyma CR. Similarly to the [Kyma Controller](../../internal/controller/kyma/controller.go), it propagates changes from the ModuleTemplate CR to the Manifest CR. The mandatory ModuleTemplate CR is not synchronized to the remote cluster and the module status does not appear in the Kyma CR status. If a mandatory module needs to be removed from all clusters, the corresponding ModuleTemplate CR needs to be deleted. The Mandatory Module Deletion Controller picks this event up and marks all associated Manifest CRs for deletion. To ensure that the ModuleTemplate CR is not removed immediately, the controller adds a finalizer to the ModuleTemplate CR. Once all associated Manifest CRs are deleted, the finalizer is removed and the ModuleTemplate CR is deleted. ## Manifest Controller diff --git a/internal/controller/mandatorymodule/deletion_controller.go b/internal/controller/mandatorymodule/deletion_controller.go index 43e3c907c0..60929d3cd2 100644 --- a/internal/controller/mandatorymodule/deletion_controller.go +++ b/internal/controller/mandatorymodule/deletion_controller.go @@ -117,7 +117,7 @@ func (r *DeletionReconciler) getCorrespondingManifests(ctx context.Context, if err := r.List(ctx, manifests, &client.ListOptions{ Namespace: template.Namespace, LabelSelector: k8slabels.SelectorFromSet(k8slabels.Set{shared.IsMandatoryModule: "true"}), - }); err != nil { + }); client.IgnoreNotFound(err) != nil { return nil, fmt.Errorf("not able to list mandatory module manifests: %w", err) } @@ -136,7 +136,9 @@ func (r *DeletionReconciler) removeManifests(ctx context.Context, manifests []v1 return nil } -func filterManifestsByAnnotation(manifests []v1beta2.Manifest, annotationKey, annotationValue string) []v1beta2.Manifest { +func filterManifestsByAnnotation(manifests []v1beta2.Manifest, + annotationKey, annotationValue string, +) []v1beta2.Manifest { filteredManifests := make([]v1beta2.Manifest, 0) for _, manifest := range manifests { if manifest.Annotations[annotationKey] == annotationValue { diff --git a/pkg/templatelookup/mandatory.go b/pkg/templatelookup/mandatory.go index 976c5852ca..599868e20e 100644 --- a/pkg/templatelookup/mandatory.go +++ b/pkg/templatelookup/mandatory.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/Masterminds/semver/v3" k8slabels "k8s.io/apimachinery/pkg/labels" "sigs.k8s.io/controller-runtime/pkg/client" @@ -22,14 +23,79 @@ func GetMandatory(ctx context.Context, kymaClient client.Reader) (ModuleTemplate return nil, fmt.Errorf("could not list mandatory ModuleTemplates: %w", err) } + // maps module name to the module template of the highest version encountered mandatoryModules := make(map[string]*ModuleTemplateInfo) for _, moduleTemplate := range mandatoryModuleTemplateList.Items { if moduleTemplate.DeletionTimestamp.IsZero() { - mandatoryModules[moduleTemplate.Name] = &ModuleTemplateInfo{ - ModuleTemplate: &moduleTemplate, + currentModuleTemplate := &moduleTemplate + moduleName := GetModuleName(currentModuleTemplate) + if mandatoryModules[moduleName] != nil { + var err error + currentModuleTemplate, err = GetModuleTemplateWithHigherVersion(currentModuleTemplate, + mandatoryModules[moduleName].ModuleTemplate) + if err != nil { + mandatoryModules[moduleName] = &ModuleTemplateInfo{ + ModuleTemplate: nil, + Err: err, + } + continue + } + } + mandatoryModules[moduleName] = &ModuleTemplateInfo{ + ModuleTemplate: currentModuleTemplate, Err: nil, } } } return mandatoryModules, nil } + +func GetModuleName(moduleTemplate *v1beta2.ModuleTemplate) string { + if moduleTemplate.Spec.ModuleName != "" { + return moduleTemplate.Spec.ModuleName + } + + // https://github.com/kyma-project/lifecycle-manager/issues/2135 + // Remove this after warden ModuleTemplate is created using modulectl + return moduleTemplate.Labels[shared.ModuleName] +} + +func GetModuleSemverVersion(moduleTemplate *v1beta2.ModuleTemplate) (*semver.Version, error) { + if moduleTemplate.Spec.Version != "" { + version, err := semver.NewVersion(moduleTemplate.Spec.Version) + if err != nil { + return nil, fmt.Errorf("could not parse version as a semver: %s: %w", + moduleTemplate.Spec.Version, err) + } + return version, nil + } + + // https://github.com/kyma-project/lifecycle-manager/issues/2135 + // Remove this after warden ModuleTemplate is created using modulectl + version, err := semver.NewVersion(moduleTemplate.Annotations[shared.ModuleVersionAnnotation]) + if err != nil { + return nil, fmt.Errorf("could not parse version as a semver %s: %w", + moduleTemplate.Annotations[shared.ModuleVersionAnnotation], err) + } + return version, nil +} + +func GetModuleTemplateWithHigherVersion(firstModuleTemplate, secondModuleTemplate *v1beta2.ModuleTemplate) (*v1beta2.ModuleTemplate, + error, +) { + firstVersion, err := GetModuleSemverVersion(firstModuleTemplate) + if err != nil { + return nil, err + } + + secondVersion, err := GetModuleSemverVersion(secondModuleTemplate) + if err != nil { + return nil, err + } + + if firstVersion.GreaterThan(secondVersion) { + return firstModuleTemplate, nil + } + + return secondModuleTemplate, nil +} diff --git a/pkg/templatelookup/mandatory_test.go b/pkg/templatelookup/mandatory_test.go new file mode 100644 index 0000000000..03ba8ffb81 --- /dev/null +++ b/pkg/templatelookup/mandatory_test.go @@ -0,0 +1,247 @@ +package templatelookup_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + machineryruntime "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/kyma-project/lifecycle-manager/api/v1beta2" + "github.com/kyma-project/lifecycle-manager/pkg/templatelookup" + "github.com/kyma-project/lifecycle-manager/pkg/testutils/builder" +) + +func TestGetDesiredModuleTemplateForMultipleVersions_ReturnCorrectValue(t *testing.T) { + firstModuleTemplate := builder.NewModuleTemplateBuilder(). + WithName("warden-1.0.0-dev"). + WithVersion("1.0.0-dev"). + WithLabel("module-diff", "first"). + Build() + + secondModuleTemplate := builder.NewModuleTemplateBuilder(). + WithName("warden-1.0.1-dev"). + WithLabel("module-diff", "second"). + WithAnnotation("operator.kyma-project.io/module-version", "1.0.1-dev"). + Build() + + result, err := templatelookup.GetModuleTemplateWithHigherVersion(firstModuleTemplate, secondModuleTemplate) + require.NoError(t, err) + require.Equal(t, secondModuleTemplate, result) +} + +func TestGetDesiredModuleTemplateForMultipleVersions_ReturnError_NotSemver(t *testing.T) { + firstModuleTemplate := builder.NewModuleTemplateBuilder(). + WithName("warden-test"). + WithVersion("test"). + WithLabel("module-diff", "first"). + Build() + + secondModuleTemplate := builder.NewModuleTemplateBuilder(). + WithName("warden-1.0.1-dev"). + WithVersion("1.0.1-dev"). + WithLabel("module-diff", "second"). + Build() + + result, err := templatelookup.GetModuleTemplateWithHigherVersion(firstModuleTemplate, secondModuleTemplate) + require.ErrorContains(t, err, "could not parse version as a semver") + require.Nil(t, result) +} + +func TestGetModuleName_withModuleName(t *testing.T) { + moduleTemplate := builder.NewModuleTemplateBuilder(). + WithModuleName("warden"). + WithLabelModuleName("warden-dev"). + Build() + + result := templatelookup.GetModuleName(moduleTemplate) + require.Equal(t, "warden", result) +} + +func TestGetModuleName_withModuleNameLabel(t *testing.T) { + moduleTemplate := builder.NewModuleTemplateBuilder(). + WithModuleName(""). + WithLabelModuleName("warden"). + Build() + + result := templatelookup.GetModuleName(moduleTemplate) + require.Equal(t, "warden", result) +} + +func TestGetModuleSemverVersion_WithCorrectSemVer_SpecVersion(t *testing.T) { + moduleTemplate := builder.NewModuleTemplateBuilder(). + WithVersion("1.0.0-dev"). + Build() + + result, err := templatelookup.GetModuleSemverVersion(moduleTemplate) + require.NoError(t, err) + require.Equal(t, "1.0.0-dev", result.String()) +} + +func TestGetModuleSemverVersion_WithCorrectSemVer_VersionAnnotation(t *testing.T) { + moduleTemplate := builder.NewModuleTemplateBuilder(). + WithAnnotation("operator.kyma-project.io/module-version", "1.0.0-dev"). + Build() + + result, err := templatelookup.GetModuleSemverVersion(moduleTemplate) + require.NoError(t, err) + require.Equal(t, "1.0.0-dev", result.String()) +} + +func TestGetModuleSemverVersion_ReturnError_NotSemver_SpecVersion(t *testing.T) { + moduleTemplate := builder.NewModuleTemplateBuilder(). + WithVersion("dev"). + Build() + + result, err := templatelookup.GetModuleSemverVersion(moduleTemplate) + require.ErrorContains(t, err, "could not parse version as a semver") + require.Nil(t, result) +} + +func TestGetModuleSemverVersion_ReturnError_NotSemver_VersionAnnotation(t *testing.T) { + moduleTemplate := builder.NewModuleTemplateBuilder(). + WithAnnotation("operator.kyma-project.io/module-version", "dev"). + Build() + + result, err := templatelookup.GetModuleSemverVersion(moduleTemplate) + require.ErrorContains(t, err, "could not parse version as a semver") + require.Nil(t, result) +} + +func TestGetMandatory_OneVersion(t *testing.T) { + scheme := machineryruntime.NewScheme() + err := v1beta2.AddToScheme(scheme) + require.NoError(t, err) + + firstModuleTemplate := builder.NewModuleTemplateBuilder(). + WithName("warden-1.0.0"). + WithModuleName("warden"). + WithMandatory(true). + WithLabel("operator.kyma-project.io/mandatory-module", "true"). + WithVersion("1.0.0"). + Build() + + secondModuleTemplate := builder.NewModuleTemplateBuilder(). + WithName("template-operator-1.0.1"). + WithVersion("1.0.0"). + Build() + + thirdModuleTemplate := builder.NewModuleTemplateBuilder(). + WithName("mandatory-1.0.1"). + WithLabelModuleName("mandatory"). + WithMandatory(true). + WithLabel("operator.kyma-project.io/mandatory-module", "true"). + WithVersion("1.0.1"). + Build() + + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(firstModuleTemplate, secondModuleTemplate, thirdModuleTemplate). + Build() + + result, err := templatelookup.GetMandatory(context.TODO(), fakeClient) + + require.NoError(t, err) + require.Len(t, result, 2) + + require.Contains(t, result, "warden") + require.Contains(t, result, "mandatory") + require.Equal(t, result["warden"].ModuleTemplate.Name, firstModuleTemplate.Name) + require.Equal(t, result["warden"].ModuleTemplate.Spec.Version, firstModuleTemplate.Spec.Version) + require.NoError(t, result["warden"].Err) + require.Equal(t, result["mandatory"].ModuleTemplate.Name, thirdModuleTemplate.Name) + require.Equal(t, result["mandatory"].ModuleTemplate.Spec.Version, thirdModuleTemplate.Spec.Version) + require.NoError(t, result["mandatory"].Err) + require.NotContains(t, result, "template-operator") +} + +func TestGetMandatory_MultipleVersions(t *testing.T) { + scheme := machineryruntime.NewScheme() + err := v1beta2.AddToScheme(scheme) + require.NoError(t, err) + + firstModuleTemplate := builder.NewModuleTemplateBuilder(). + WithName("warden-1.0.0"). + WithModuleName("warden"). + WithMandatory(true). + WithLabel("operator.kyma-project.io/mandatory-module", "true"). + WithVersion("1.0.0"). + Build() + + secondModuleTemplate := builder.NewModuleTemplateBuilder(). + WithName("template-operator-1.0.1"). + WithVersion("1.0.0"). + Build() + + thirdModuleTemplate := builder.NewModuleTemplateBuilder(). + WithName("mandatory-1.0.1"). + WithLabelModuleName("mandatory"). + WithMandatory(true). + WithLabel("operator.kyma-project.io/mandatory-module", "true"). + WithVersion("1.0.1"). + Build() + + fourthModuleTemplate := builder.NewModuleTemplateBuilder(). + WithName("warden-1.0.1"). + WithModuleName("warden"). + WithMandatory(true). + WithLabel("operator.kyma-project.io/mandatory-module", "true"). + WithVersion("1.0.1"). + Build() + + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(firstModuleTemplate, secondModuleTemplate, thirdModuleTemplate, fourthModuleTemplate). + Build() + + result, err := templatelookup.GetMandatory(context.TODO(), fakeClient) + + require.NoError(t, err) + require.Len(t, result, 2) + + require.Contains(t, result, "warden") + require.Contains(t, result, "mandatory") + require.Equal(t, result["warden"].ModuleTemplate.Name, fourthModuleTemplate.Name) + require.Equal(t, result["warden"].ModuleTemplate.Spec.Version, fourthModuleTemplate.Spec.Version) + require.NoError(t, result["warden"].Err) + require.Equal(t, result["mandatory"].ModuleTemplate.Name, thirdModuleTemplate.Name) + require.Equal(t, result["mandatory"].ModuleTemplate.Spec.Version, thirdModuleTemplate.Spec.Version) + require.NoError(t, result["mandatory"].Err) + require.NotContains(t, result, "template-operator") +} + +func TestGetMandatory_WithErrorNotSemVer(t *testing.T) { + scheme := machineryruntime.NewScheme() + err := v1beta2.AddToScheme(scheme) + require.NoError(t, err) + + firstModuleTemplate := builder.NewModuleTemplateBuilder(). + WithName("warden-test"). + WithModuleName("warden"). + WithMandatory(true). + WithLabel("operator.kyma-project.io/mandatory-module", "true"). + WithVersion("test"). + Build() + + secondModuleTemplate := builder.NewModuleTemplateBuilder(). + WithName("warden-1.0.0"). + WithModuleName("warden"). + WithMandatory(true). + WithLabel("operator.kyma-project.io/mandatory-module", "true"). + WithVersion("1.0.0"). + Build() + + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(firstModuleTemplate, secondModuleTemplate). + Build() + + result, err := templatelookup.GetMandatory(context.TODO(), fakeClient) + + require.NoError(t, err) + require.Len(t, result, 1) + + require.Contains(t, result, "warden") + require.ErrorContains(t, result["warden"].Err, "could not parse version as a semver") +} diff --git a/pkg/testutils/deployment.go b/pkg/testutils/deployment.go index a5dbc304be..4cd691ab0c 100644 --- a/pkg/testutils/deployment.go +++ b/pkg/testutils/deployment.go @@ -24,7 +24,6 @@ func DeploymentIsReady(ctx context.Context, clnt client.Client, name, namespace } return fmt.Errorf("could not get deployment: %w", err) } - if deploy.Spec.Replicas != nil && *deploy.Spec.Replicas == deploy.Status.ReadyReplicas { return nil @@ -75,4 +74,5 @@ func GetDeployment(ctx context.Context, clnt client.Client, } return deploy, nil } + func int32Ptr(i int32) *int32 { return &i } diff --git a/pkg/testutils/manifest.go b/pkg/testutils/manifest.go index c1cb38b0e5..663cd8dd58 100644 --- a/pkg/testutils/manifest.go +++ b/pkg/testutils/manifest.go @@ -44,6 +44,7 @@ var ( errManifestOperationNotContainMessage = errors.New("manifest last operation does not contain expected message") errManifestVersionIsIncorrect = errors.New("manifest version is incorrect") errManifestConditionNotExists = errors.New("manifest condition does not exist") + errManifestNotFound = errors.New("manifest does not exist") ) func NewTestManifest(prefix string) *v1beta2.Manifest { @@ -292,6 +293,36 @@ func SetSkipLabelToMandatoryManifests(ctx context.Context, clnt client.Client, i return nil } +func MandatoryModuleManifestExistWithCorrectVersion(ctx context.Context, clnt client.Client, + moduleName, expectedVersion string, +) error { + manifestList := v1beta2.ManifestList{} + if err := clnt.List(ctx, &manifestList, &client.ListOptions{ + LabelSelector: k8slabels.SelectorFromSet(k8slabels.Set{shared.IsMandatoryModule: "true"}), + }); err != nil { + return fmt.Errorf("failed to list manifests: %w", err) + } + + manifestFound := false + for _, manifest := range manifestList.Items { + manifestModuleName, err := manifest.GetModuleName() + if err != nil { + return fmt.Errorf("failed to get manifest module name, %w", err) + } + if manifestModuleName == moduleName { + manifestFound = true + if manifest.Spec.Version != expectedVersion { + return errManifestVersionIsIncorrect + } + } + } + + if !manifestFound { + return errManifestNotFound + } + return nil +} + func SkipLabelExistsInManifest(ctx context.Context, clnt client.Client, kymaName, diff --git a/scripts/tests/deploy_moduletemplate.sh b/scripts/tests/deploy_moduletemplate.sh index df7714a339..f70edce432 100755 --- a/scripts/tests/deploy_moduletemplate.sh +++ b/scripts/tests/deploy_moduletemplate.sh @@ -7,6 +7,8 @@ set -o pipefail MODULE_NAME=$1 RELEASE_VERSION=$2 INCLUDE_DEFAULT_CR=${3:-true} +MANDATORY=${4:-false} +DEPLOY_MODULETEMPLATE=${5:-true} cat < module-config-for-e2e.yaml name: kyma-project.io/module/${MODULE_NAME} @@ -26,16 +28,25 @@ defaultCR: https://localhost:8080/config/samples/default-sample-cr.yaml EOF fi +if [ "${MANDATORY}" == "true" ]; then + cat <> module-config-for-e2e.yaml +mandatory: true +EOF +fi + cat module-config-for-e2e.yaml modulectl create --config-file ./module-config-for-e2e.yaml --registry http://localhost:5111 --insecure sed -i 's/localhost:5111/k3d-kcp-registry.localhost:5000/g' ./template.yaml -kubectl apply -f template.yaml cat template.yaml echo "ModuleTemplate created successfully" +if [ "${DEPLOY_MODULETEMPLATE}" == "true" ]; then +kubectl apply -f template.yaml +rm -f template.yaml +fi + rm -f module-config-for-e2e.yaml rm -f template-operator.yaml -rm -f template.yaml rm -f default-sample-cr.yaml echo "Temporary files removed successfully" diff --git a/tests/e2e/Makefile b/tests/e2e/Makefile index 7c4d3f166f..1ff6225ea9 100644 --- a/tests/e2e/Makefile +++ b/tests/e2e/Makefile @@ -79,6 +79,9 @@ kyma-metrics: mandatory-module-metrics: go test -timeout 20m -ginkgo.v -ginkgo.focus "Mandatory Module Metrics" +mandatory-module-metrics-with-old-naming-pattern: + go test -timeout 20m -ginkgo.v -ginkgo.focus "Mandatory Module With Old Naming Pattern Metrics" + watcher-enqueue: go test -timeout 20m -ginkgo.v -ginkgo.focus "Enqueue Event from Watcher" @@ -97,6 +100,9 @@ module-consistency: mandatory-module: go test -timeout 20m -ginkgo.v -ginkgo.focus "Mandatory Module Installation and Deletion" +mandatory-module-with-old-naming-pattern: + go test -timeout 20m -ginkgo.v -ginkgo.focus "Mandatory Module With Old Naming Pattern Installation and Deletion" + non-blocking-deletion: go test -timeout 20m -ginkgo.v -ginkgo.focus "Non Blocking Kyma Module Deletion" diff --git a/tests/e2e/mandatory_module_test.go b/tests/e2e/mandatory_module_test.go index baf8b98059..af03f8e8b8 100644 --- a/tests/e2e/mandatory_module_test.go +++ b/tests/e2e/mandatory_module_test.go @@ -3,15 +3,14 @@ package e2e_test import ( "os/exec" - templatev1alpha1 "github.com/kyma-project/template-operator/api/v1alpha1" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/kyma-project/lifecycle-manager/api/shared" "github.com/kyma-project/lifecycle-manager/api/v1beta2" + templatev1alpha1 "github.com/kyma-project/template-operator/api/v1alpha1" + apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" . "github.com/kyma-project/lifecycle-manager/pkg/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" ) var _ = Describe("Mandatory Module Installation and Deletion", Ordered, func() { @@ -54,6 +53,13 @@ var _ = Describe("Mandatory Module Installation and Deletion", Ordered, func() { shared.OperatorGroup, "v1beta2", string(shared.ModuleTemplateKind), skrClient). Should(Not(Succeed())) }) + + By("And the mandatory module manifest is installed with the correct version", func() { + Consistently(MandatoryModuleManifestExistWithCorrectVersion). + WithContext(ctx). + WithArguments(kcpClient, "template-operator", "1.1.0-smoke-test"). + Should(Succeed()) + }) }) It("When the mandatory Manifest is labelled to skip reconciliation", func() { @@ -137,10 +143,10 @@ var _ = Describe("Mandatory Module Installation and Deletion", Ordered, func() { }). Should(Succeed()) }) - It("Then Kyma is in a \"Error\" State", func() { + It("Then Kyma is in a \"Warning\" State", func() { Eventually(KymaIsInState). WithContext(ctx). - WithArguments(kyma.GetName(), kyma.GetNamespace(), kcpClient, shared.StateError). + WithArguments(kyma.GetName(), kyma.GetNamespace(), kcpClient, shared.StateWarning). Should(Succeed()) }) @@ -160,7 +166,7 @@ var _ = Describe("Mandatory Module Installation and Deletion", Ordered, func() { It("When new version of ModuleTemplate is applied", func() { cmd := exec.Command("kubectl", "apply", "-f", - "./moduletemplate/mandatory_moduletemplate_template_operator_v2.yaml") + "mandatory_template_v2.yaml") _, err := cmd.CombinedOutput() Expect(err).NotTo(HaveOccurred()) }) @@ -184,15 +190,33 @@ var _ = Describe("Mandatory Module Installation and Deletion", Ordered, func() { WithArguments(kyma.GetName(), kyma.GetNamespace(), kcpClient, shared.StateReady). Should(Succeed()) }) + + By("And the mandatory module manifest is installed with the correct version", func() { + Consistently(MandatoryModuleManifestExistWithCorrectVersion). + WithContext(ctx). + WithArguments(kcpClient, "template-operator", "2.4.1-smoke-test"). + Should(Succeed()) + }) }) - It("When the mandatory ModuleTemplate is removed", func() { + It("When the mandatory ModuleTemplates are removed", func() { + Eventually(DeleteCR). + WithContext(ctx). + WithArguments(kcpClient, + &v1beta2.ModuleTemplate{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: "template-operator-1.1.0-smoke-test", + Namespace: ControlPlaneNamespace, + }, + }). + Should(Succeed()) + Eventually(DeleteCR). WithContext(ctx). WithArguments(kcpClient, &v1beta2.ModuleTemplate{ ObjectMeta: apimetav1.ObjectMeta{ - Name: "template-operator-mandatory", + Name: "template-operator-2.4.1-smoke-test", Namespace: ControlPlaneNamespace, }, }). diff --git a/tests/e2e/mandatory_module_with_old_naming_pattern_test.go b/tests/e2e/mandatory_module_with_old_naming_pattern_test.go new file mode 100644 index 0000000000..12366a2e47 --- /dev/null +++ b/tests/e2e/mandatory_module_with_old_naming_pattern_test.go @@ -0,0 +1,223 @@ +package e2e_test + +import ( + "os/exec" + + "github.com/kyma-project/lifecycle-manager/api/shared" + "github.com/kyma-project/lifecycle-manager/api/v1beta2" + templatev1alpha1 "github.com/kyma-project/template-operator/api/v1alpha1" + apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + . "github.com/kyma-project/lifecycle-manager/pkg/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Mandatory Module With Old Naming Pattern Installation and Deletion", Ordered, func() { + kyma := NewKymaWithSyncLabel("kyma-sample", ControlPlaneNamespace, v1beta2.DefaultChannel) + + InitEmptyKymaBeforeAll(kyma) + CleanupKymaAfterAll(kyma) + + Context("Given kyma deployed in KCP", func() { + It("Then mandatory module is installed on the SKR cluster", func() { + Eventually(DeploymentIsReady). + WithContext(ctx). + WithArguments(skrClient, ModuleDeploymentNameInOlderVersion, + TestModuleResourceNamespace). + Should(Succeed()) + By("And the SKR Module Default CR is in a \"Ready\" State", func() { + Eventually(CheckSampleCRIsInState). + WithContext(ctx). + WithArguments(TestModuleCRName, RemoteNamespace, skrClient, shared.StateReady). + Should(Succeed()) + }) + By("And the KCP Kyma CR is in a \"Ready\" State", func() { + Consistently(KymaIsInState). + WithContext(ctx). + WithArguments(kyma.GetName(), kyma.GetNamespace(), kcpClient, shared.StateReady). + Should(Succeed()) + }) + By("And the Mandatory ModuleTemplate has the correct mandatory-module label", func() { + Eventually(MandatoryModuleTemplateHasExpectedLabel). + WithContext(ctx). + WithArguments(kcpClient, "template-operator", shared.IsMandatoryModule, + shared.EnableLabelValue). + Should(Succeed()) + }) + + By("And the mandatory ModuleTemplate is not synchronised to the SKR cluster", func() { + Consistently(CheckIfExists). + WithContext(ctx). + WithArguments("template-operator-mandatory", RemoteNamespace, + shared.OperatorGroup, "v1beta2", string(shared.ModuleTemplateKind), skrClient). + Should(Not(Succeed())) + }) + }) + + It("When the mandatory Manifest is labelled to skip reconciliation", func() { + Eventually(SetSkipLabelToMandatoryManifests). + WithContext(ctx). + WithArguments(kcpClient, true). + Should(Succeed()) + + By("And deleting the mandatory SKR Default CR", func() { + Eventually(DeleteCRWithGVK). + WithContext(ctx). + WithArguments(skrClient, TestModuleCRName, RemoteNamespace, templatev1alpha1.GroupVersion.Group, + "v1alpha1", string(templatev1alpha1.SampleKind)). + Should(Succeed()) + }) + }) + It("Then mandatory SKR Module Default CR is not recreated", func() { + Consistently(CheckIfExists). + WithContext(ctx). + WithArguments(TestModuleCRName, RemoteNamespace, templatev1alpha1.GroupVersion.Group, + "v1alpha1", string(templatev1alpha1.SampleKind), skrClient). + Should(Equal(ErrNotFound)) + }) + + It("When deleting the mandatory SKR Module Manager Deployment", func() { + err := DeleteCRWithGVK(ctx, skrClient, ModuleDeploymentNameInOlderVersion, + TestModuleResourceNamespace, "apps", "v1", "Deployment") + Expect(err).ToNot(HaveOccurred()) + }) + It("Then Module Manager Deployment is not recreated on the SKR cluster", func() { + Eventually(DeploymentIsReady). + WithContext(ctx). + WithArguments(skrClient, ModuleDeploymentNameInOlderVersion, + TestModuleResourceNamespace). + Should(Equal(ErrNotFound)) + By("And the KCP Kyma CR is in a \"Ready\" State", func() { + Consistently(KymaIsInState). + WithContext(ctx). + WithArguments(kyma.GetName(), kyma.GetNamespace(), kcpClient, shared.StateReady). + Should(Succeed()) + }) + }) + + It("When the mandatory Manifest skip reconciliation label is removed", func() { + Eventually(SetSkipLabelToMandatoryManifests). + WithContext(ctx). + WithArguments(kcpClient, false). + Should(Succeed()) + }) + It("Then mandatory SKR Module Default CR is recreated", func() { + Eventually(CheckIfExists). + WithContext(ctx). + WithArguments(TestModuleCRName, RemoteNamespace, + templatev1alpha1.GroupVersion.Group, "v1alpha1", string(templatev1alpha1.SampleKind), + skrClient). + Should(Succeed()) + + By("And mandatory SKR Module Deployment is recreated", func() { + Eventually(DeploymentIsReady). + WithContext(ctx). + WithArguments(skrClient, ModuleDeploymentNameInOlderVersion, + TestModuleResourceNamespace). + Should(Succeed()) + }) + + By("And the KCP Kyma CR is in a \"Ready\" State", func() { + Consistently(KymaIsInState). + WithContext(ctx). + WithArguments(kyma.GetName(), kyma.GetNamespace(), kcpClient, shared.StateReady). + Should(Succeed()) + }) + }) + + It("When mandatory Module is enabled on SKR Kyma CR", func() { + Eventually(EnableModule). + WithContext(ctx). + WithArguments(skrClient, defaultRemoteKymaName, RemoteNamespace, v1beta2.Module{ + Name: TestModuleName, + Channel: v1beta2.DefaultChannel, + Managed: true, + }). + Should(Succeed()) + }) + It("Then Kyma is in a \"Error\" State", func() { + Eventually(KymaIsInState). + WithContext(ctx). + WithArguments(kyma.GetName(), kyma.GetNamespace(), kcpClient, shared.StateError). + Should(Succeed()) + }) + + It("When mandatory Module is disabled on SKR Kyma CR", func() { + Eventually(DisableModule). + WithContext(ctx). + WithArguments(skrClient, defaultRemoteKymaName, RemoteNamespace, TestModuleName). + Should(Succeed()) + + By("Then Kyma is back in a \"Ready\" State", func() { + Eventually(KymaIsInState). + WithContext(ctx). + WithArguments(kyma.GetName(), kyma.GetNamespace(), kcpClient, shared.StateReady). + Should(Succeed()) + }) + }) + + It("When new version of ModuleTemplate is applied", func() { + cmd := exec.Command("kubectl", "apply", "-f", + "./moduletemplate/mandatory_moduletemplate_template_operator_v2.yaml") + _, err := cmd.CombinedOutput() + Expect(err).NotTo(HaveOccurred()) + }) + It("Then Kyma mandatory Module is updated on SKR Cluster", func() { + Eventually(DeploymentIsReady). + WithContext(ctx). + WithArguments(skrClient, ModuleDeploymentNameInNewerVersion, + TestModuleResourceNamespace). + Should(Succeed()) + + By("And old Module Operator Deployment is removed", func() { + Eventually(DeploymentIsReady). + WithContext(ctx). + WithArguments(skrClient, ModuleDeploymentNameInOlderVersion, + TestModuleResourceNamespace). + Should(Equal(ErrNotFound)) + }) + By("And the KCP Kyma CR is in a \"Ready\" State", func() { + Consistently(KymaIsInState). + WithContext(ctx). + WithArguments(kyma.GetName(), kyma.GetNamespace(), kcpClient, shared.StateReady). + Should(Succeed()) + }) + }) + + It("When the mandatory ModuleTemplate is removed", func() { + Eventually(DeleteCR). + WithContext(ctx). + WithArguments(kcpClient, + &v1beta2.ModuleTemplate{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: "template-operator-mandatory", + Namespace: ControlPlaneNamespace, + }, + }). + Should(Succeed()) + }) + It("Then mandatory SKR module is removed", func() { + Eventually(DeploymentIsReady). + WithContext(ctx). + WithArguments(skrClient, ModuleDeploymentNameInNewerVersion, + TestModuleResourceNamespace). + Should(Equal(ErrNotFound)) + + By("And the mandatory SKR Module Default CR is removed", func() { + Eventually(CheckIfExists). + WithContext(ctx). + WithArguments(TestModuleCRName, RemoteNamespace, + templatev1alpha1.GroupVersion.Group, "v1alpha1", string(templatev1alpha1.SampleKind), + skrClient). + Should(Equal(ErrNotFound)) + }) + By("And the KCP Kyma CR is in a \"Ready\" State", func() { + Eventually(KymaIsInState). + WithContext(ctx). + WithArguments(kyma.GetName(), kyma.GetNamespace(), kcpClient, shared.StateReady). + Should(Succeed()) + }) + }) + }) +}) diff --git a/tests/e2e/mandatory_modules_metrics_test.go b/tests/e2e/mandatory_modules_metrics_test.go index fe35ce6638..37c12cab3f 100644 --- a/tests/e2e/mandatory_modules_metrics_test.go +++ b/tests/e2e/mandatory_modules_metrics_test.go @@ -1,3 +1,4 @@ +//nolint:dupl //this test will be deleted during this issue https://github.com/kyma-project/lifecycle-manager/issues/2060 package e2e_test import ( @@ -58,12 +59,13 @@ var _ = Describe("Mandatory Module Metrics", Ordered, func() { WithArguments(kcpClient, &v1beta2.ModuleTemplate{ ObjectMeta: apimetav1.ObjectMeta{ - Name: "template-operator-mandatory", + Name: "template-operator-1.1.0-smoke-test", Namespace: "kcp-system", }, }). Should(Succeed()) }) + It("Then mandatory SKR module is removed", func() { Eventually(DeploymentIsReady). WithContext(ctx). diff --git a/tests/e2e/mandatory_modules_with_old_naming_pattern_metrics_test.go b/tests/e2e/mandatory_modules_with_old_naming_pattern_metrics_test.go new file mode 100644 index 0000000000..17d3b881b5 --- /dev/null +++ b/tests/e2e/mandatory_modules_with_old_naming_pattern_metrics_test.go @@ -0,0 +1,104 @@ +//nolint:dupl //this test will be deleted during this issue https://github.com/kyma-project/lifecycle-manager/issues/2060 +package e2e_test + +import ( + apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kyma-project/lifecycle-manager/api/shared" + "github.com/kyma-project/lifecycle-manager/api/v1beta2" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + . "github.com/kyma-project/lifecycle-manager/pkg/testutils" +) + +var _ = Describe("Mandatory Module With Old Naming Pattern Metrics", Ordered, func() { + kyma := NewKymaWithSyncLabel("kyma-sample", "kcp-system", v1beta2.DefaultChannel) + + InitEmptyKymaBeforeAll(kyma) + CleanupKymaAfterAll(kyma) + + Context("Given SKR Cluster", func() { + It("Then mandatory module is installed on the SKR cluster", func() { + Eventually(DeploymentIsReady). + WithContext(ctx). + WithArguments(skrClient, ModuleDeploymentNameInOlderVersion, + TestModuleResourceNamespace). + Should(Succeed()) + By("And the SKR Module Default CR is in a \"Ready\" State", func() { + Eventually(CheckSampleCRIsInState). + WithContext(ctx). + WithArguments("sample-yaml", "kyma-system", skrClient, shared.StateReady). + Should(Succeed()) + }) + By("And the KCP Kyma CR is in a \"Ready\" State", func() { + Eventually(KymaIsInState). + WithContext(ctx). + WithArguments(kyma.GetName(), kyma.GetNamespace(), kcpClient, shared.StateReady). + Should(Succeed()) + }) + + By("And count of Mandatory Module State Metric in \"Ready\" State is 1", func() { + Eventually(GetMandatoryModuleStateMetric). + WithContext(ctx). + WithArguments(kyma.GetName(), TestModuleName, string(shared.StateReady)). + Should(Equal(1)) + }) + + By("And count of Mandatory ModuleTemplates Metric is 1", func() { + Eventually(GetMandatoryModuleTemplateCountMetric). + WithContext(ctx). + Should(Equal(1)) + }) + }) + + It("When the mandatory ModuleTemplate is removed", func() { + Eventually(DeleteCR). + WithContext(ctx). + WithArguments(kcpClient, + &v1beta2.ModuleTemplate{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: "template-operator-mandatory", + Namespace: "kcp-system", + }, + }). + Should(Succeed()) + }) + + It("Then mandatory SKR module is removed", func() { + Eventually(DeploymentIsReady). + WithContext(ctx). + WithArguments(skrClient, ModuleDeploymentNameInOlderVersion, + TestModuleResourceNamespace). + Should(Equal(ErrNotFound)) + + By("And the mandatory SKR Module Default CR is removed", func() { + Eventually(CheckIfExists). + WithContext(ctx). + WithArguments("sample-yaml", "kyma-system", + "operator.kyma-project.io", "v1alpha1", "Sample", skrClient). + Should(Equal(ErrNotFound)) + }) + By("And the KCP Kyma CR is in a \"Ready\" State", func() { + Eventually(KymaIsInState). + WithContext(ctx). + WithArguments(kyma.GetName(), kyma.GetNamespace(), kcpClient, shared.StateReady). + Should(Succeed()) + }) + + By("And count of Mandatory Module State Metric in \"Ready\" State is 0", func() { + Eventually(GetMandatoryModuleStateMetric). + WithContext(ctx). + WithArguments(kyma.GetName(), TestModuleName, string(shared.StateReady)). + Should(Equal(0)) + }) + + By("And count of Mandatory ModuleTemplates Metric is 0", func() { + Eventually(GetMandatoryModuleTemplateCountMetric). + WithContext(ctx). + Should(Equal(0)) + }) + }) + }) +}) diff --git a/tests/e2e/moduletemplate/mandatory_moduletemplate_template_operator_v1.yaml b/tests/e2e/moduletemplate/mandatory_moduletemplate_template_operator_v1.yaml index 6753f78d48..99220a9f7a 100644 --- a/tests/e2e/moduletemplate/mandatory_moduletemplate_template_operator_v1.yaml +++ b/tests/e2e/moduletemplate/mandatory_moduletemplate_template_operator_v1.yaml @@ -5,6 +5,8 @@ metadata: namespace: kcp-system labels: "operator.kyma-project.io/module-name": "template-operator" + annotations: + "operator.kyma-project.io/module-version": "1.1.0-smoke-test" spec: mandatory: true channel: regular diff --git a/tests/e2e/moduletemplate/mandatory_moduletemplate_template_operator_v2.yaml b/tests/e2e/moduletemplate/mandatory_moduletemplate_template_operator_v2.yaml index 2e462df6fb..d412564f4d 100755 --- a/tests/e2e/moduletemplate/mandatory_moduletemplate_template_operator_v2.yaml +++ b/tests/e2e/moduletemplate/mandatory_moduletemplate_template_operator_v2.yaml @@ -7,6 +7,7 @@ metadata: "operator.kyma-project.io/module-name": "template-operator" annotations: "operator.kyma-project.io/is-cluster-scoped": "false" + "operator.kyma-project.io/module-version": "2.4.1-smoke-test" spec: mandatory: true channel: regular diff --git a/unit-test-coverage.yaml b/unit-test-coverage.yaml index 7b2b9439de..d6d9faa527 100644 --- a/unit-test-coverage.yaml +++ b/unit-test-coverage.yaml @@ -18,4 +18,4 @@ packages: internal/pkg/resources: 91.7 internal/remote: 20.2 internal/util/collections: 86 - pkg/templatelookup: 77.1 + pkg/templatelookup: 83.3