From 9491cf4b96ee99d959af783cee3cd4bc2098b0f9 Mon Sep 17 00:00:00 2001 From: Nesma Badr Date: Wed, 16 Oct 2024 11:02:59 +0200 Subject: [PATCH] feat: Use ModuleReleaseMeta in Kyma reconciliation loop instead of moduletemplate.channel (#1947) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add implementation * Add unit test * Adjust test * Adjust test * Fix linting issue * Fix linting issue * Add rbac for modulereleasemeta * Fix rbac * Fix unit tests * Fix role * Fix linting * Fix unit test * Fix role * Add E2E for ModuleReleaseMeta * Fix E2E test * Fix E2E test * Your commit message here * Add documentation * Fix non-blocking deletion test * Debug test * Debug test * Add test case for old moduletemplate name with modulereleasemeta * Xin's comments * Xin's comments * Fix E2E test * Update docs/contributor/02-controllers.md Co-authored-by: Małgorzata Świeca * Enhance doc * Update docs/contributor/02-controllers.md Co-authored-by: Małgorzata Świeca * Update docs/contributor/02-controllers.md Co-authored-by: Małgorzata Świeca --------- Co-authored-by: Małgorzata Świeca --- .../action.yml | 284 ++++++++++++++++++ .../test-e2e-with-modulereleasemeta.yml | 104 +++++++ api/shared/cr_kind.go | 9 +- config/rbac/common/role.yaml | 7 + docs/contributor/02-controllers.md | 7 + internal/cache_options.go | 5 + internal/controller/kyma/controller.go | 1 + pkg/templatelookup/modulereleasemeta.go | 46 +++ pkg/templatelookup/modulereleasemeta_test.go | 54 ++++ pkg/templatelookup/regular.go | 91 +++++- pkg/templatelookup/regular_test.go | 269 +++++++++++++++-- pkg/testutils/builder/modulereleasemeta.go | 50 +++ pkg/testutils/kyma.go | 18 ++ pkg/testutils/modulereleasemeta.go | 60 ++++ pkg/testutils/moduletemplate.go | 7 +- tests/e2e/Makefile | 5 +- tests/e2e/module_deletion_upgrade_test.go | 6 + tests/e2e/module_upgrade_new_version_test.go | 6 + ...emeta_with_obsolete_moduletemplate_test.go | 50 +++ tests/e2e/rbac_privileges_test.go | 5 + 20 files changed, 1035 insertions(+), 49 deletions(-) create mode 100644 .github/actions/deploy-template-operator-with-modulereleasemeta/action.yml create mode 100644 .github/workflows/test-e2e-with-modulereleasemeta.yml create mode 100644 pkg/templatelookup/modulereleasemeta.go create mode 100644 pkg/templatelookup/modulereleasemeta_test.go create mode 100644 pkg/testutils/builder/modulereleasemeta.go create mode 100644 pkg/testutils/modulereleasemeta.go create mode 100644 tests/e2e/modulereleasemeta_with_obsolete_moduletemplate_test.go diff --git a/.github/actions/deploy-template-operator-with-modulereleasemeta/action.yml b/.github/actions/deploy-template-operator-with-modulereleasemeta/action.yml new file mode 100644 index 0000000000..9a0c7c3e0a --- /dev/null +++ b/.github/actions/deploy-template-operator-with-modulereleasemeta/action.yml @@ -0,0 +1,284 @@ +name: Deploy template-operator With ModuleReleaseMeta +description: Deploys a test-specific template-operator and corresponding ModuleReleaseMeta. +runs: + using: composite + steps: + - name: Create Template Operator Module and apply ModuleTemplate and ModuleReleaseMeta + working-directory: template-operator + if: ${{ matrix.e2e-test == 'kyma-metrics' || + matrix.e2e-test == 'non-blocking-deletion' || + matrix.e2e-test == 'purge-controller' || + matrix.e2e-test == 'purge-metrics' || + matrix.e2e-test == 'kyma-deprovision-with-foreground-propagation' || + matrix.e2e-test == 'kyma-deprovision-with-background-propagation' || + matrix.e2e-test == 'module-consistency' || + matrix.e2e-test == 'skip-manifest-reconciliation' || + matrix.e2e-test == 'misconfigured-kyma-secret' || + matrix.e2e-test == 'unmanage-module' + }} + shell: bash + run: | + make build-manifests + kyma alpha create module --module-config-file ./module-config.yaml --path . --registry localhost:5111 --insecure + sed -i 's/localhost:5111/k3d-kcp-registry.localhost:5000/g' ./template.yaml + sed -i 's/template-operator-regular/template-operator-1.0.1/g' ./template.yaml + kubectl get crds + kubectl apply -f template.yaml + kubectl apply -f module-release-meta.yaml + - name: Apply Template Operator Module and ModuleReleaseMeta for regular and fast channels + working-directory: lifecycle-manager + if: ${{ matrix.e2e-test == 'module-upgrade-channel-switch' || + matrix.e2e-test == 'module-upgrade-new-version' || + matrix.e2e-test == 'upgrade-under-deletion' + }} + shell: bash + run: | + sed -i 's/template-operator-fast/template-operator-2.4.2-e2e-test/g' tests/moduletemplates/moduletemplate_template_operator_v2_fast.yaml + sed -i 's/template-operator-regular/template-operator-1.1.1-e2e-test/g' tests/moduletemplates/moduletemplate_template_operator_v1_regular.yaml + sed -i 's/template-operator-regular/template-operator-2.4.2-e2e-test/g' tests/moduletemplates/moduletemplate_template_operator_v2_regular_new_version.yaml + + kubectl apply -f tests/moduletemplates/moduletemplate_template_operator_v2_fast.yaml + kubectl apply -f tests/moduletemplates/moduletemplate_template_operator_v1_regular.yaml + + cat < mrm.yaml + apiVersion: operator.kyma-project.io/v1beta2 + kind: ModuleReleaseMeta + metadata: + name: template-operator + namespace: kcp-system + spec: + channels: + - channel: fast + version: 2.4.2-e2e-test + - channel: regular + version: 1.1.1-e2e-test + moduleName: template-operator + EOF + + kubectl apply -f mrm.yaml + - name: Create Template Operator Module for installation by version + working-directory: lifecycle-manager + if: ${{ matrix.e2e-test == 'module-install-by-version' }} + shell: bash + run: | + kubectl apply -f tests/moduletemplates/moduletemplate_template_operator_v2_fast.yaml + kubectl apply -f tests/moduletemplates/moduletemplate_template_operator_v1_regular.yaml + kubectl apply -f tests/moduletemplates/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' + }} + shell: bash + run: | + kubectl apply -f tests/moduletemplates/mandatory_moduletemplate_template_operator_v1.yaml + - name: Apply Template Operator Module V2 and ModuleReleaseMeta, fast channel + working-directory: ./lifecycle-manager + if: ${{ matrix.e2e-test == 'non-blocking-deletion' }} + shell: bash + run: | + sed -i 's/template-operator-fast/template-operator-2.4.2-e2e-test/g' tests/moduletemplates/moduletemplate_template_operator_v2_fast.yaml + kubectl apply -f tests/moduletemplates/moduletemplate_template_operator_v2_fast.yaml + cat < mrm.yaml + apiVersion: operator.kyma-project.io/v1beta2 + kind: ModuleReleaseMeta + metadata: + name: template-operator + namespace: kcp-system + spec: + channels: + - channel: fast + version: 2.4.2-e2e-test + - channel: regular + version: 1.0.1 + moduleName: template-operator + EOF + kubectl apply -f mrm.yaml + - name: Create Template Operator Module with Deployment, with final state and final deletion state as `Warning` and apply + working-directory: template-operator + if: ${{ matrix.e2e-test == 'module-status-decoupling-with-deployment'}} + shell: bash + run: | + pushd config/overlays/deployment + echo \ + "- op: replace + path: /spec/template/spec/containers/0/args/1 + value: --final-state=Warning + - op: replace + path: /spec/template/spec/containers/0/args/2 + value: --final-deletion-state=Warning" >> warning_patch.yaml + cat warning_patch.yaml + kustomize edit add patch --path warning_patch.yaml --kind Deployment + popd + make build-manifests + kyma alpha create module --module-config-file ./module-config.yaml --path . --registry localhost:5111 --insecure + sed -i 's/localhost:5111/k3d-kcp-registry.localhost:5000/g' ./template.yaml + sed -i 's/template-operator-regular/template-operator-1.0.1/g' ./template.yaml + kubectl get crds + kubectl apply -f template.yaml + kubectl apply -f module-release-meta.yaml + - name: Create Template Operator Module with StatefulSet, with final state and final deletion state as `Warning` and apply + working-directory: template-operator + if: ${{ matrix.e2e-test == 'module-status-decoupling-with-statefulset'}} + shell: bash + run: | + pushd config/overlays/statefulset + echo \ + "- op: replace + path: /spec/template/spec/containers/0/args/1 + value: --final-state=Warning + - op: replace + path: /spec/template/spec/containers/0/args/2 + value: --final-deletion-state=Warning" >> warning_patch.yaml + cat warning_patch.yaml + kustomize edit add patch --path warning_patch.yaml --kind StatefulSet + popd + make build-statefulset-manifests + kyma alpha create module --module-config-file ./module-config.yaml --path . --registry localhost:5111 --insecure + sed -i 's/localhost:5111/k3d-kcp-registry.localhost:5000/g' ./template.yaml + sed -i 's/template-operator-regular/template-operator-1.0.1/g' ./template.yaml + kubectl get crds + kubectl apply -f template.yaml + kubectl apply -f module-release-meta.yaml + - name: Create non-working image patch for Template Operator Module and create associated module config file and applying ModuleReleaseMeta + working-directory: template-operator + if: ${{ matrix.e2e-test == 'module-status-decoupling-with-deployment'|| + matrix.e2e-test == 'module-status-decoupling-with-statefulset'}} + shell: bash + run: | + echo "name: kyma-project.io/module/template-operator-misconfigured + channel: regular + version: v1.1.1 + manifest: template-operator.yaml + security: sec-scanners-config.yaml + defaultCR: ./config/samples/default-sample-cr.yaml + annotations: + operator.kyma-project.io/doc-url: https://kyma-project.io" >> misconfigured-module-config.yaml + + cat < mrm.yaml + apiVersion: operator.kyma-project.io/v1beta2 + kind: ModuleReleaseMeta + metadata: + name: template-operator-misconfigured + namespace: kcp-system + spec: + channels: + - channel: regular + version: 1.1.1 + moduleName: template-operator-misconfigured + EOF + + kubectl apply -f mrm.yaml + - name: Create Template Operator Module with Deployment, with non-working image and apply + working-directory: template-operator + if: ${{ matrix.e2e-test == 'module-status-decoupling-with-deployment'}} + shell: bash + run: | + pushd config/overlays/deployment + echo \ + "- op: replace + path: /spec/template/spec/containers/0/image + value: non-working-path" >> image_patch.yaml + cat image_patch.yaml + kustomize edit add patch --path image_patch.yaml --kind Deployment + popd + make build-manifests + kyma alpha create module --module-config-file ./misconfigured-module-config.yaml --path . --registry localhost:5111 --insecure + sed -i 's/localhost:5111/k3d-kcp-registry.localhost:5000/g' ./template.yaml + sed -i 's/template-operator-misconfigured-regular/template-operator-misconfigured-1.1.1/g' ./template.yaml + kubectl get crds + kubectl apply -f template.yaml + - name: Create Template Operator Module with StatefulSet, with non-working image and apply + working-directory: template-operator + if: ${{ matrix.e2e-test == 'module-status-decoupling-with-statefulset'}} + shell: bash + run: | + pushd config/overlays/statefulset + echo \ + "- op: replace + path: /spec/template/spec/containers/0/image + value: non-working-path" >> image_patch.yaml + cat image_patch.yaml + kustomize edit add patch --path image_patch.yaml --kind StatefulSet + popd + make build-statefulset-manifests + kyma alpha create module --module-config-file ./misconfigured-module-config.yaml --path . --registry localhost:5111 --insecure + sed -i 's/localhost:5111/k3d-kcp-registry.localhost:5000/g' ./template.yaml + sed -i 's/template-operator-misconfigured-regular/template-operator-misconfigured-1.1.1/g' ./template.yaml + kubectl get crds + kubectl apply -f template.yaml + + - name: Create Template Operator Module without default CR and apply ModuleReleaseMeta + working-directory: template-operator + if: ${{ matrix.e2e-test == 'module-without-default-cr' }} + shell: bash + run: | + make build-manifests + echo "name: kyma-project.io/module/template-operator + channel: regular + version: v1.0.0 + manifest: template-operator.yaml + security: sec-scanners-config.yaml + annotations: + operator.kyma-project.io/doc-url: https://kyma-project.io" >> module-config-no-cr.yaml + kyma alpha create module \ + --module-config-file ./module-config-no-cr.yaml \ + --path . \ + --registry localhost:5111 \ + --insecure + sed -i 's/localhost:5111/k3d-kcp-registry.localhost:5000/g' ./template.yaml + sed -i 's/template-operator-regular/template-operator-1.0.0/g' ./template.yaml + kubectl get crds + kubectl apply -f template.yaml + cat < mrm.yaml + apiVersion: operator.kyma-project.io/v1beta2 + kind: ModuleReleaseMeta + metadata: + name: template-operator + namespace: kcp-system + spec: + channels: + - channel: regular + version: 1.0.0 + moduleName: template-operator + EOF + + kubectl apply -f mrm.yaml + + - name: Apply ModuleReleaseMeta and Template Operator Module in OCM format + working-directory: ./lifecycle-manager + if: ${{ matrix.e2e-test == 'ocm-compatible-module-template' }} + shell: bash + run: | + sed -i 's/template-operator-regular/template-operator-1.0.0-new-ocm-format/g' tests/moduletemplates/moduletemplate_template_operator_regular_new_ocm.yaml + kubectl apply -f tests/moduletemplates/moduletemplate_template_operator_regular_new_ocm.yaml + cat < mrm.yaml + apiVersion: operator.kyma-project.io/v1beta2 + kind: ModuleReleaseMeta + metadata: + name: template-operator + namespace: kcp-system + spec: + channels: + - channel: regular + version: 1.0.0-new-ocm-format + moduleName: template-operator + EOF + + kubectl apply -f mrm.yaml + + - name: Apply ModuleReleaseMeta with ModuleTemplate with name - + working-directory: template-operator + if: ${{ matrix.e2e-test == 'modulereleasemeta-with-obsolete-moduletemplate' }} + shell: bash + run: | + make build-manifests + kyma alpha create module --module-config-file ./module-config.yaml --path . --registry localhost:5111 --insecure + sed -i 's/localhost:5111/k3d-kcp-registry.localhost:5000/g' ./template.yaml + kubectl apply -f template.yaml + kubectl apply -f module-release-meta.yaml + + + + + diff --git a/.github/workflows/test-e2e-with-modulereleasemeta.yml b/.github/workflows/test-e2e-with-modulereleasemeta.yml new file mode 100644 index 0000000000..1b9b9f41c7 --- /dev/null +++ b/.github/workflows/test-e2e-with-modulereleasemeta.yml @@ -0,0 +1,104 @@ +name: TestSuite E2E with ModuleReleaseMeta + +env: + IMAGE_REPO: europe-docker.pkg.dev/kyma-project/dev/lifecycle-manager +on: + workflow_dispatch: + inputs: + k8s_version: + description: With Kubernetes version + required: false + pull_request: + types: [ opened, edited, synchronize, reopened, ready_for_review ] +jobs: + wait-for-image-build: + name: Wait for image build + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Wait for the Docker image + timeout-minutes: 20 + env: + ITERATIONS: 40 + SLEEP_SECONDS: 30 + run: ./.github/scripts/release/wait_for_image.sh ${{ env.IMAGE_REPO }}:${{ github.event.pull_request.head.sha }} ${{ env.ITERATIONS }} ${{ env.SLEEP_SECONDS}} + e2e-integration: + name: E2E With ModuleReleaseMeta + needs: wait-for-image-build + strategy: + fail-fast: false + matrix: + e2e-test: + - watcher-enqueue + - kyma-deprovision-with-foreground-propagation + - kyma-deprovision-with-background-propagation + - module-status-decoupling-with-statefulset + - module-status-decoupling-with-deployment + - kyma-metrics + - module-without-default-cr + - module-consistency + - non-blocking-deletion + - upgrade-under-deletion + - purge-controller + - purge-metrics + - module-upgrade-channel-switch + - module-upgrade-new-version + - unmanage-module + - skip-manifest-reconciliation + - ca-certificate-rotation + - self-signed-certificate-rotation + - mandatory-module + - mandatory-module-metrics + - misconfigured-kyma-secret + - rbac-privileges + - ocm-compatible-module-template + - modulereleasemeta-with-obsolete-moduletemplate + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout lifecycle-manager + uses: actions/checkout@v4 + with: + path: lifecycle-manager + + - name: Checkout template-operator + uses: actions/checkout@v4 + with: + repository: kyma-project/template-operator + path: template-operator + + - name: Get configuration + uses: ./lifecycle-manager/.github/actions/get-configuration + id: configuration + + - name: Setup tools + uses: ./lifecycle-manager/.github/actions/setup-tools + with: + k8s_version: ${{ steps.configuration.outputs.k8s_version }} + istio_version: ${{ steps.configuration.outputs.istio_version }} + k3d_version: ${{ steps.configuration.outputs.k3d_version }} + go-version-file: lifecycle-manager/go.mod + cache-dependency-path: lifecycle-manager/go.sum + + - name: Setup test clusters + uses: ./lifecycle-manager/.github/actions/setup-test-clusters + with: + k8s_version: ${{ steps.configuration.outputs.k8s_version }} + cert_manager_version: ${{ steps.configuration.outputs.cert_manager_version }} + + - name: Deploy lifecycle-manager + uses: ./lifecycle-manager/.github/actions/deploy-lifecycle-manager-e2e + with: + klm_version_tag: ${{ steps.configuration.outputs.klm_version_tag }} + klm_image_repo: ${{ steps.configuration.outputs.klm_image_repo }} + + - name: Deploy template-operator + uses: ./lifecycle-manager/.github/actions/deploy-template-operator-with-modulereleasemeta + + - name: Run '${{ matrix.e2e-test }}' test + working-directory: lifecycle-manager + run: | + make -C tests/e2e ${{ matrix.e2e-test }} diff --git a/api/shared/cr_kind.go b/api/shared/cr_kind.go index 2e1fd0fe37..a9117efa49 100644 --- a/api/shared/cr_kind.go +++ b/api/shared/cr_kind.go @@ -3,10 +3,11 @@ package shared import "strings" const ( - KymaKind Kind = "Kyma" - ModuleTemplateKind Kind = "ModuleTemplate" - WatcherKind Kind = "Watcher" - ManifestKind Kind = "Manifest" + KymaKind Kind = "Kyma" + ModuleTemplateKind Kind = "ModuleTemplate" + WatcherKind Kind = "Watcher" + ManifestKind Kind = "Manifest" + ModuleReleaseMetaKind Kind = "ModuleReleaseMeta" ) type Kind string diff --git a/config/rbac/common/role.yaml b/config/rbac/common/role.yaml index 7283131823..859cbfbeca 100644 --- a/config/rbac/common/role.yaml +++ b/config/rbac/common/role.yaml @@ -149,6 +149,13 @@ rules: - get - patch - update +- apiGroups: + - operator.kyma-project.io + resources: + - modulereleasemetas + verbs: + - get + - list - apiGroups: - operator.kyma-project.io resources: diff --git a/docs/contributor/02-controllers.md b/docs/contributor/02-controllers.md index 310f9a91be..2ef111d222 100644 --- a/docs/contributor/02-controllers.md +++ b/docs/contributor/02-controllers.md @@ -16,6 +16,13 @@ Its main responsibilities are: To determine the cluster to sync to, fields in **.spec.remote** are evaluated. This allows the use of ModuleTemplate CRs in a cluster managed by Lifecycle Manager while Kyma Control Plane is in a different cluster. +### Fetching the ModuleTemplate + +Kyma Controller uses the ModuleReleaseMeta CR to fetch the correct ModuleTemplate CR for a module. The name of ModuleReleaseMeta CR should be the same as the module name. Kyma Controller uses the channel defined in the Kyma CR spec to fetch the corresponding module version from the ModuleReleaseMeta channel-version pairs. Kyma Controller then fetches the ModuleTemplate CR with the module name-version from the ModuleTemplate CRs available in the Kyma Control Plane. If there is no entry in the ModuleReleaseMeta CR for the channel defined in the Kyma CR spec, the Kyma CR will be in the `Error` state indicating that no versions were found for the channel. + +If a ModuleReleaseMeta CR for a particular module doesn't exist, Kyma Controller lists all the ModuleTemplates in the Control Plane and then filters them using the **.spec.channel** parameter in the Kyma CR. + + ### Remote Synchronization The Kyma CR in Kyma Control Plane shows the initial specification and the current status. To install a module, Lifecycle Manager uses the specification from the remote cluster Kyma CR. diff --git a/internal/cache_options.go b/internal/cache_options.go index ab9ac7559f..cc7a64a017 100644 --- a/internal/cache_options.go +++ b/internal/cache_options.go @@ -57,6 +57,11 @@ func (c *KcpCacheOptions) GetCacheOptions() cache.Options { c.kcpNamespace: {}, }, }, + &v1beta2.ModuleReleaseMeta{}: { + Namespaces: map[string]cache.Config{ + c.kcpNamespace: {}, + }, + }, &v1beta2.Manifest{}: { Namespaces: map[string]cache.Config{ c.kcpNamespace: {}, diff --git a/internal/controller/kyma/controller.go b/internal/controller/kyma/controller.go index 55b6c833a6..1aac27abae 100644 --- a/internal/controller/kyma/controller.go +++ b/internal/controller/kyma/controller.go @@ -80,6 +80,7 @@ type Reconciler struct { // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch // +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=operator.kyma-project.io,resources=moduletemplates,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=operator.kyma-project.io,resources=modulereleasemetas,verbs=get;list // +kubebuilder:rbac:groups=operator.kyma-project.io,resources=moduletemplates/finalizers,verbs=update // +kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=get;list;watch // +kubebuilder:rbac:groups=cert-manager.io,resources=issuers,verbs=get;list;watch diff --git a/pkg/templatelookup/modulereleasemeta.go b/pkg/templatelookup/modulereleasemeta.go new file mode 100644 index 0000000000..ecf16aab65 --- /dev/null +++ b/pkg/templatelookup/modulereleasemeta.go @@ -0,0 +1,46 @@ +package templatelookup + +import ( + "context" + "errors" + "fmt" + + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/kyma-project/lifecycle-manager/api/v1beta2" +) + +var ( + ErrChannelNotFound = errors.New("no versions found for channel") + ErrNoChannelsFound = errors.New("no channels found for module") +) + +func GetModuleReleaseMeta(ctx context.Context, clnt client.Reader, moduleName string, + namespace string) (*v1beta2.ModuleReleaseMeta, + error, +) { + mrm := &v1beta2.ModuleReleaseMeta{} + if err := clnt.Get(ctx, client.ObjectKey{ + Namespace: namespace, + Name: moduleName, + }, mrm); err != nil { + return nil, fmt.Errorf("failed to fetch ModuleReleaseMeta for %s: %w", moduleName, err) + } + + return mrm, nil +} + +func GetChannelVersionForModule(moduleReleaseMeta *v1beta2.ModuleReleaseMeta, desiredChannel string) (string, error) { + channelAssignments := moduleReleaseMeta.Spec.Channels + if len(channelAssignments) == 0 { + return "", fmt.Errorf("%w: %s", ErrNoChannelsFound, moduleReleaseMeta.Name) + } + + for _, channelAssignment := range channelAssignments { + if channelAssignment.Channel == desiredChannel { + return channelAssignment.Version, nil + } + } + + return "", fmt.Errorf("%w: %s in module %s", ErrChannelNotFound, desiredChannel, moduleReleaseMeta.Name) +} diff --git a/pkg/templatelookup/modulereleasemeta_test.go b/pkg/templatelookup/modulereleasemeta_test.go new file mode 100644 index 0000000000..ec344aa654 --- /dev/null +++ b/pkg/templatelookup/modulereleasemeta_test.go @@ -0,0 +1,54 @@ +package templatelookup_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/kyma-project/lifecycle-manager/api/v1beta2" + "github.com/kyma-project/lifecycle-manager/pkg/templatelookup" +) + +func Test_GetChannelVersionForModule_WhenEmptyChannels(t *testing.T) { + moduleReleaseMeta := &v1beta2.ModuleReleaseMeta{ + Spec: v1beta2.ModuleReleaseMetaSpec{ + Channels: nil, + }, + } + _, err := templatelookup.GetChannelVersionForModule(moduleReleaseMeta, "test") + + require.ErrorIs(t, err, templatelookup.ErrNoChannelsFound) +} + +func Test_GetChannelVersionForModule_WhenChannelFound(t *testing.T) { + moduleReleaseMeta := &v1beta2.ModuleReleaseMeta{ + Spec: v1beta2.ModuleReleaseMetaSpec{ + Channels: []v1beta2.ChannelVersionAssignment{ + { + Channel: "regular", + Version: "1.0.0", + }, + }, + }, + } + version, err := templatelookup.GetChannelVersionForModule(moduleReleaseMeta, "regular") + + require.NoError(t, err) + require.Equal(t, "1.0.0", version) +} + +func Test_GetChannelVersionForModule_WhenChannelNotFound(t *testing.T) { + moduleReleaseMeta := &v1beta2.ModuleReleaseMeta{ + Spec: v1beta2.ModuleReleaseMetaSpec{ + Channels: []v1beta2.ChannelVersionAssignment{ + { + Channel: "regular", + Version: "1.0.0", + }, + }, + }, + } + _, err := templatelookup.GetChannelVersionForModule(moduleReleaseMeta, "fast") + + require.ErrorIs(t, err, templatelookup.ErrChannelNotFound) +} diff --git a/pkg/templatelookup/regular.go b/pkg/templatelookup/regular.go index 8c16c60b4b..e99df6c059 100644 --- a/pkg/templatelookup/regular.go +++ b/pkg/templatelookup/regular.go @@ -13,6 +13,7 @@ import ( "github.com/kyma-project/lifecycle-manager/api/v1beta2" "github.com/kyma-project/lifecycle-manager/internal/descriptor/provider" "github.com/kyma-project/lifecycle-manager/pkg/log" + "github.com/kyma-project/lifecycle-manager/pkg/util" ) var ( @@ -56,14 +57,7 @@ func (t *TemplateLookup) GetRegularTemplates(ctx context.Context, kyma *v1beta2. continue } - var templateInfo ModuleTemplateInfo - - if module.IsInstalledByVersion() { - templateInfo = t.GetAndValidateByVersion(ctx, module.Name, module.Version) - } else { - templateInfo = t.GetAndValidateByChannel(ctx, module.Name, module.Channel, kyma.Spec.Channel) - } - + templateInfo := t.PopulateModuleTemplateInfo(ctx, module, kyma.Namespace, kyma.Spec.Channel) templateInfo = ValidateTemplateMode(templateInfo, kyma) if templateInfo.Err != nil { templates[module.Name] = &templateInfo @@ -91,6 +85,56 @@ func (t *TemplateLookup) GetRegularTemplates(ctx context.Context, kyma *v1beta2. return templates } +func (t *TemplateLookup) PopulateModuleTemplateInfo(ctx context.Context, + module AvailableModule, namespace, kymaChannel string, +) ModuleTemplateInfo { + moduleReleaseMeta, err := GetModuleReleaseMeta(ctx, t, module.Name, namespace) + if util.IsNotFound(err) { + return t.populateModuleTemplateInfoWithoutModuleReleaseMeta(ctx, module, kymaChannel) + } + + if err != nil { + return ModuleTemplateInfo{Err: err} + } + + return t.populateModuleTemplateInfoUsingModuleReleaseMeta(ctx, module, moduleReleaseMeta, kymaChannel, namespace) +} + +func (t *TemplateLookup) populateModuleTemplateInfoWithoutModuleReleaseMeta(ctx context.Context, + module AvailableModule, kymaChannel string, +) ModuleTemplateInfo { + var templateInfo ModuleTemplateInfo + if module.IsInstalledByVersion() { + templateInfo = t.GetAndValidateByVersion(ctx, module.Name, module.Version) + } else { + templateInfo = t.GetAndValidateByChannel(ctx, module.Name, module.Channel, kymaChannel) + } + return templateInfo +} + +func (t *TemplateLookup) populateModuleTemplateInfoUsingModuleReleaseMeta(ctx context.Context, + module AvailableModule, + moduleReleaseMeta *v1beta2.ModuleReleaseMeta, kymaChannel, namespace string, +) ModuleTemplateInfo { + var templateInfo ModuleTemplateInfo + templateInfo.DesiredChannel = getDesiredChannel(module.Channel, kymaChannel) + desiredModuleVersion, err := GetChannelVersionForModule(moduleReleaseMeta, templateInfo.DesiredChannel) + if err != nil { + templateInfo.Err = err + return templateInfo + } + + template, err := t.getTemplateByVersion(ctx, module.Name, desiredModuleVersion, namespace) + if err != nil { + templateInfo.Err = err + return templateInfo + } + + templateInfo.ModuleTemplate = template + + return templateInfo +} + func ValidateTemplateMode(template ModuleTemplateInfo, kyma *v1beta2.Kyma) ModuleTemplateInfo { if template.Err != nil { return template @@ -106,13 +150,31 @@ func ValidateTemplateMode(template ModuleTemplateInfo, kyma *v1beta2.Kyma) Modul return template } -func (t *TemplateLookup) GetAndValidateByChannel(ctx context.Context, name, channel, defaultChannel string) ModuleTemplateInfo { +func (t *TemplateLookup) getTemplateByVersion(ctx context.Context, + moduleName, moduleVersion, namespace string, +) (*v1beta2.ModuleTemplate, error) { + moduleTemplate := &v1beta2.ModuleTemplate{} + + moduleTemplateName := fmt.Sprintf("%s-%s", moduleName, moduleVersion) + if err := t.Get(ctx, client.ObjectKey{ + Name: moduleTemplateName, + Namespace: namespace, + }, moduleTemplate); err != nil { + return nil, fmt.Errorf("failed to get module template: %w", err) + } + + return moduleTemplate, nil +} + +func (t *TemplateLookup) GetAndValidateByChannel(ctx context.Context, + name, channel, defaultChannel string, +) ModuleTemplateInfo { desiredChannel := getDesiredChannel(channel, defaultChannel) info := ModuleTemplateInfo{ DesiredChannel: desiredChannel, } - template, err := t.getTemplateByChannel(ctx, name, desiredChannel) + template, err := t.filterTemplatesByChannel(ctx, name, desiredChannel) if err != nil { info.Err = err return info @@ -136,7 +198,7 @@ func (t *TemplateLookup) GetAndValidateByVersion(ctx context.Context, name, vers info := ModuleTemplateInfo{ DesiredChannel: string(shared.NoneChannel), } - template, err := t.getTemplateByVersion(ctx, name, version) + template, err := t.filterTemplatesByVersion(ctx, name, version) if err != nil { info.Err = err return info @@ -246,7 +308,7 @@ func getDesiredChannel(moduleChannel, globalChannel string) string { return desiredChannel } -func (t *TemplateLookup) getTemplateByChannel(ctx context.Context, name, desiredChannel string) ( +func (t *TemplateLookup) filterTemplatesByChannel(ctx context.Context, name, desiredChannel string) ( *v1beta2.ModuleTemplate, error, ) { templateList := &v1beta2.ModuleTemplateList{} @@ -280,7 +342,7 @@ func (t *TemplateLookup) getTemplateByChannel(ctx context.Context, name, desired return filteredTemplates[0], nil } -func (t *TemplateLookup) getTemplateByVersion(ctx context.Context, name, version string) ( +func (t *TemplateLookup) filterTemplatesByVersion(ctx context.Context, name, version string) ( *v1beta2.ModuleTemplate, error, ) { templateList := &v1beta2.ModuleTemplateList{} @@ -291,7 +353,8 @@ func (t *TemplateLookup) getTemplateByVersion(ctx context.Context, name, version var filteredTemplates []*v1beta2.ModuleTemplate for _, template := range templateList.Items { - if TemplateNameMatch(&template, name) && shared.NoneChannel.Equals(template.Spec.Channel) && template.Spec.Version == version { + if TemplateNameMatch(&template, + name) && shared.NoneChannel.Equals(template.Spec.Channel) && template.Spec.Version == version { filteredTemplates = append(filteredTemplates, &template) continue } diff --git a/pkg/templatelookup/regular_test.go b/pkg/templatelookup/regular_test.go index 713088a54e..d1519b74c1 100644 --- a/pkg/templatelookup/regular_test.go +++ b/pkg/templatelookup/regular_test.go @@ -3,11 +3,14 @@ package templatelookup_test import ( "context" "errors" + "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + apierrors "k8s.io/apimachinery/pkg/api/errors" apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "ocm.software/ocm/api/ocm/compdesc" ocmmetav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" compdescv2 "ocm.software/ocm/api/ocm/compdesc/versions/v2" @@ -30,12 +33,16 @@ const ( ) type FakeModuleTemplateReader struct { - templateList v1beta2.ModuleTemplateList + templateList v1beta2.ModuleTemplateList + moduleReleaseMetaList v1beta2.ModuleReleaseMetaList } -func NewFakeModuleTemplateReader(templateList v1beta2.ModuleTemplateList) *FakeModuleTemplateReader { +func NewFakeModuleTemplateReader(templateList v1beta2.ModuleTemplateList, + moduleReleaseMetaList v1beta2.ModuleReleaseMetaList, +) *FakeModuleTemplateReader { return &FakeModuleTemplateReader{ - templateList: templateList, + templateList: templateList, + moduleReleaseMetaList: moduleReleaseMetaList, } } @@ -48,9 +55,29 @@ func (f *FakeModuleTemplateReader) List(_ context.Context, list client.ObjectLis return nil } -func (f *FakeModuleTemplateReader) Get(_ context.Context, _ client.ObjectKey, _ client.Object, +func (f *FakeModuleTemplateReader) Get(_ context.Context, objKey client.ObjectKey, obj client.Object, _ ...client.GetOption, ) error { + notFoundErr := apierrors.NewNotFound(schema.GroupResource{}, objKey.Name) + if castedObj, ok := obj.(*v1beta2.ModuleReleaseMeta); ok { + for _, mrm := range f.moduleReleaseMetaList.Items { + if mrm.Name == objKey.Name { + *castedObj = mrm + return nil + } + } + + return notFoundErr + } else if castedObj, ok := obj.(*v1beta2.ModuleTemplate); ok { + for _, template := range f.templateList.Items { + if template.Name == objKey.Name { + *castedObj = template + return nil + } + } + return notFoundErr + } + return nil } @@ -155,13 +182,63 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchModuleChannel(t *testing.T testModule := testutils.NewTestModule("module1", "new_channel") tests := []struct { - name string - kyma *v1beta2.Kyma - availableModuleTemplate v1beta2.ModuleTemplateList - want templatelookup.ModuleTemplatesByModuleName + name string + kyma *v1beta2.Kyma + availableModuleTemplate v1beta2.ModuleTemplateList + availableModuleReleaseMeta v1beta2.ModuleReleaseMetaList + want templatelookup.ModuleTemplatesByModuleName }{ { - name: "When upgrade version during channel switch, Then result contains no error", + name: "When upgrade version during channel switch, Then result contains no error, without ModuleReleaseMeta", + kyma: builder.NewKymaBuilder(). + WithEnabledModule(testModule). + WithModuleStatus(v1beta2.ModuleStatus{ + Name: testModule.Name, + Channel: v1beta2.DefaultChannel, + Version: version1, + Template: &v1beta2.TrackingObject{ + PartialMeta: v1beta2.PartialMeta{ + Generation: 1, + }, + }, + }).Build(), + availableModuleTemplate: generateModuleTemplateListWithModule(testModule.Name, testModule.Channel, + version2), + availableModuleReleaseMeta: v1beta2.ModuleReleaseMetaList{}, + want: templatelookup.ModuleTemplatesByModuleName{ + testModule.Name: &templatelookup.ModuleTemplateInfo{ + DesiredChannel: testModule.Channel, + Err: nil, + }, + }, + }, + { + name: "When downgrade version during channel switch, Then result contains error, without ModuleReleaseMeta", + kyma: builder.NewKymaBuilder(). + WithEnabledModule(testModule). + WithModuleStatus(v1beta2.ModuleStatus{ + Name: testModule.Name, + Channel: v1beta2.DefaultChannel, + Version: version2, + Template: &v1beta2.TrackingObject{ + PartialMeta: v1beta2.PartialMeta{ + Generation: 1, + }, + }, + }).Build(), + availableModuleTemplate: generateModuleTemplateListWithModule(testModule.Name, testModule.Channel, + version1), + availableModuleReleaseMeta: v1beta2.ModuleReleaseMetaList{}, + + want: templatelookup.ModuleTemplatesByModuleName{ + testModule.Name: &templatelookup.ModuleTemplateInfo{ + DesiredChannel: testModule.Channel, + Err: templatelookup.ErrTemplateUpdateNotAllowed, + }, + }, + }, + { + name: "When upgrade version during channel switch, Then result contains no error, with ModuleReleaseMeta", kyma: builder.NewKymaBuilder(). WithEnabledModule(testModule). WithModuleStatus(v1beta2.ModuleStatus{ @@ -176,14 +253,20 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchModuleChannel(t *testing.T }).Build(), availableModuleTemplate: generateModuleTemplateListWithModule(testModule.Name, testModule.Channel, version2), + availableModuleReleaseMeta: generateModuleReleaseMetaList(testModule.Name, + []v1beta2.ChannelVersionAssignment{ + {Channel: testModule.Channel, Version: version2}, + {Channel: v1beta2.DefaultChannel, Version: version1}, + }), want: templatelookup.ModuleTemplatesByModuleName{ testModule.Name: &templatelookup.ModuleTemplateInfo{ DesiredChannel: testModule.Channel, Err: nil, }, }, - }, { - name: "When downgrade version during channel switch, Then result contains error", + }, + { + name: "When downgrade version during channel switch, Then result contains error, with ModuleReleaseMeta", kyma: builder.NewKymaBuilder(). WithEnabledModule(testModule). WithModuleStatus(v1beta2.ModuleStatus{ @@ -198,6 +281,10 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchModuleChannel(t *testing.T }).Build(), availableModuleTemplate: generateModuleTemplateListWithModule(testModule.Name, testModule.Channel, version1), + availableModuleReleaseMeta: generateModuleReleaseMetaList(testModule.Name, + []v1beta2.ChannelVersionAssignment{ + {Channel: testModule.Channel, Version: version1}, + }), want: templatelookup.ModuleTemplatesByModuleName{ testModule.Name: &templatelookup.ModuleTemplateInfo{ DesiredChannel: testModule.Channel, @@ -209,7 +296,8 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchModuleChannel(t *testing.T for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { - lookup := templatelookup.NewTemplateLookup(NewFakeModuleTemplateReader(testCase.availableModuleTemplate), + lookup := templatelookup.NewTemplateLookup(NewFakeModuleTemplateReader(testCase.availableModuleTemplate, + testCase.availableModuleReleaseMeta), provider.NewCachedDescriptorProvider()) got := lookup.GetRegularTemplates(context.TODO(), testCase.kyma) assert.Equal(t, len(got), len(testCase.want)) @@ -235,6 +323,8 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchBetweenModuleVersions(t *t Add(moduleToInstall.Name, string(shared.NoneChannel), version3). Build() + availableModuleReleaseMetas := v1beta2.ModuleReleaseMetaList{} + tests := []struct { name string kyma *v1beta2.Kyma @@ -279,7 +369,8 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchBetweenModuleVersions(t *t for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { - lookup := templatelookup.NewTemplateLookup(NewFakeModuleTemplateReader(availableModuleTemplates), + lookup := templatelookup.NewTemplateLookup(NewFakeModuleTemplateReader(availableModuleTemplates, + availableModuleReleaseMetas), provider.NewCachedDescriptorProvider()) got := lookup.GetRegularTemplates(context.TODO(), testCase.kyma) assert.Len(t, got, 1) @@ -307,6 +398,8 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchFromChannelToVersion(t *te Add(moduleToInstall.Name, string(shared.NoneChannel), version3). Build() + availableModuleReleaseMetas := v1beta2.ModuleReleaseMetaList{} + tests := []struct { name string kyma *v1beta2.Kyma @@ -368,7 +461,8 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchFromChannelToVersion(t *te for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { - lookup := templatelookup.NewTemplateLookup(NewFakeModuleTemplateReader(availableModuleTemplates), + lookup := templatelookup.NewTemplateLookup(NewFakeModuleTemplateReader(availableModuleTemplates, + availableModuleReleaseMetas), provider.NewCachedDescriptorProvider()) got := lookup.GetRegularTemplates(context.TODO(), testCase.kyma) assert.Len(t, got, 1) @@ -396,6 +490,8 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchFromVersionToChannel(t *te Add(moduleToInstall.Name, string(shared.NoneChannel), version3). Build() + availableModuleReleaseMetas := v1beta2.ModuleReleaseMetaList{} + tests := []struct { name string kyma *v1beta2.Kyma @@ -457,7 +553,8 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchFromVersionToChannel(t *te for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { - lookup := templatelookup.NewTemplateLookup(NewFakeModuleTemplateReader(availableModuleTemplates), + lookup := templatelookup.NewTemplateLookup(NewFakeModuleTemplateReader(availableModuleTemplates, + availableModuleReleaseMetas), provider.NewCachedDescriptorProvider()) got := lookup.GetRegularTemplates(context.TODO(), testCase.kyma) assert.Len(t, got, 1) @@ -476,16 +573,17 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchFromVersionToChannel(t *te func TestNewTemplateLookup_GetRegularTemplates_WhenModuleTemplateContainsInvalidDescriptor(t *testing.T) { testModule := testutils.NewTestModule("module1", v1beta2.DefaultChannel) - tests := []struct { - name string - kyma *v1beta2.Kyma - want templatelookup.ModuleTemplatesByModuleName + name string + kyma *v1beta2.Kyma + mrmExists bool + want templatelookup.ModuleTemplatesByModuleName }{ { - name: "When module enabled in Spec, then return ModuleTemplatesByModuleName with error", + name: "When module enabled in Spec, then return ModuleTemplatesByModuleName with error, without ModuleReleaseMeta", kyma: builder.NewKymaBuilder(). WithEnabledModule(testModule).Build(), + mrmExists: false, want: templatelookup.ModuleTemplatesByModuleName{ testModule.Name: &templatelookup.ModuleTemplateInfo{ DesiredChannel: testModule.Channel, @@ -494,7 +592,39 @@ func TestNewTemplateLookup_GetRegularTemplates_WhenModuleTemplateContainsInvalid }, }, { - name: "When module exits in ModuleStatus only, then return ModuleTemplatesByModuleName with error", + name: "When module exits in ModuleStatus only, then return ModuleTemplatesByModuleName with error, without ModuleReleaseMeta", + kyma: builder.NewKymaBuilder(). + WithModuleStatus(v1beta2.ModuleStatus{ + Name: testModule.Name, + Channel: testModule.Channel, + Template: &v1beta2.TrackingObject{ + PartialMeta: v1beta2.PartialMeta{ + Generation: 1, + }, + }, + }).Build(), + mrmExists: false, + want: templatelookup.ModuleTemplatesByModuleName{ + testModule.Name: &templatelookup.ModuleTemplateInfo{ + DesiredChannel: testModule.Channel, + Err: provider.ErrDecode, + }, + }, + }, + { + name: "When module enabled in Spec, then return ModuleTemplatesByModuleName with error, with ModuleReleaseMeta", + kyma: builder.NewKymaBuilder(). + WithEnabledModule(testModule).Build(), + mrmExists: true, + want: templatelookup.ModuleTemplatesByModuleName{ + testModule.Name: &templatelookup.ModuleTemplateInfo{ + DesiredChannel: testModule.Channel, + Err: provider.ErrDecode, + }, + }, + }, + { + name: "When module exits in ModuleStatus only, then return ModuleTemplatesByModuleName with error, with ModuleReleaseMeta", kyma: builder.NewKymaBuilder(). WithModuleStatus(v1beta2.ModuleStatus{ Name: testModule.Name, @@ -505,6 +635,7 @@ func TestNewTemplateLookup_GetRegularTemplates_WhenModuleTemplateContainsInvalid }, }, }).Build(), + mrmExists: true, want: templatelookup.ModuleTemplatesByModuleName{ testModule.Name: &templatelookup.ModuleTemplateInfo{ DesiredChannel: testModule.Channel, @@ -516,15 +647,27 @@ func TestNewTemplateLookup_GetRegularTemplates_WhenModuleTemplateContainsInvalid for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { givenTemplateList := &v1beta2.ModuleTemplateList{} + moduleReleaseMetas := v1beta2.ModuleReleaseMetaList{} for _, module := range templatelookup.FindAvailableModules(testCase.kyma) { givenTemplateList.Items = append(givenTemplateList.Items, *builder.NewModuleTemplateBuilder(). + WithName(fmt.Sprintf("%s-%s", module.Name, testModule.Version)). WithModuleName(module.Name). WithLabelModuleName(module.Name). WithChannel(module.Channel). WithDescriptor(nil). WithRawDescriptor([]byte("{invalid_json}")).Build()) + + if testCase.mrmExists { + moduleReleaseMetas.Items = append(moduleReleaseMetas.Items, + *builder.NewModuleReleaseMetaBuilder(). + WithModuleName(module.Name). + WithModuleChannelAndVersions([]v1beta2.ChannelVersionAssignment{ + {Channel: module.Channel, Version: testModule.Version}, + }).Build()) + } } - lookup := templatelookup.NewTemplateLookup(NewFakeModuleTemplateReader(*givenTemplateList), + lookup := templatelookup.NewTemplateLookup(NewFakeModuleTemplateReader(*givenTemplateList, + moduleReleaseMetas), provider.NewCachedDescriptorProvider()) got := lookup.GetRegularTemplates(context.TODO(), testCase.kyma) assert.Equal(t, len(got), len(testCase.want)) @@ -585,7 +728,8 @@ func TestTemplateLookup_GetRegularTemplates_WhenModuleTemplateNotFound(t *testin for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { givenTemplateList := &v1beta2.ModuleTemplateList{} - lookup := templatelookup.NewTemplateLookup(NewFakeModuleTemplateReader(*givenTemplateList), + lookup := templatelookup.NewTemplateLookup(NewFakeModuleTemplateReader(*givenTemplateList, + v1beta2.ModuleReleaseMetaList{}), provider.NewCachedDescriptorProvider()) got := lookup.GetRegularTemplates(context.TODO(), testCase.kyma) assert.Equal(t, len(got), len(testCase.want)) @@ -604,14 +748,57 @@ func TestTemplateLookup_GetRegularTemplates_WhenModuleTemplateExists(t *testing. testModule := testutils.NewTestModule("module1", v1beta2.DefaultChannel) tests := []struct { - name string - kyma *v1beta2.Kyma - want templatelookup.ModuleTemplatesByModuleName + name string + kyma *v1beta2.Kyma + mrmExist bool + want templatelookup.ModuleTemplatesByModuleName }{ { - name: "When module enabled in Spec, then return expected moduleTemplateInfo", + name: "When module enabled in Spec, then return expected moduleTemplateInfo, without ModuleReleaseMeta", + kyma: builder.NewKymaBuilder(). + WithEnabledModule(testModule).Build(), + mrmExist: false, + want: templatelookup.ModuleTemplatesByModuleName{ + testModule.Name: &templatelookup.ModuleTemplateInfo{ + DesiredChannel: testModule.Channel, + Err: nil, + ModuleTemplate: builder.NewModuleTemplateBuilder(). + WithModuleName(testModule.Name). + WithChannel(testModule.Channel). + Build(), + }, + }, + }, + { + name: "When module enabled in Spec, then return expected moduleTemplateInfo, with ModuleReleaseMeta", kyma: builder.NewKymaBuilder(). WithEnabledModule(testModule).Build(), + mrmExist: true, + want: templatelookup.ModuleTemplatesByModuleName{ + testModule.Name: &templatelookup.ModuleTemplateInfo{ + DesiredChannel: testModule.Channel, + Err: nil, + ModuleTemplate: builder.NewModuleTemplateBuilder(). + WithModuleName(testModule.Name). + WithChannel(testModule.Channel). + Build(), + }, + }, + }, + { + name: "When module exits in ModuleStatus only, then return expected moduleTemplateInfo, without ModuleReleaseMeta", + kyma: builder.NewKymaBuilder(). + WithEnabledModule(testModule). + WithModuleStatus(v1beta2.ModuleStatus{ + Name: testModule.Name, + Channel: testModule.Channel, + Template: &v1beta2.TrackingObject{ + PartialMeta: v1beta2.PartialMeta{ + Generation: 1, + }, + }, + }).Build(), + mrmExist: false, want: templatelookup.ModuleTemplatesByModuleName{ testModule.Name: &templatelookup.ModuleTemplateInfo{ DesiredChannel: testModule.Channel, @@ -624,7 +811,7 @@ func TestTemplateLookup_GetRegularTemplates_WhenModuleTemplateExists(t *testing. }, }, { - name: "When module exits in ModuleStatus only, then return expected moduleTemplateInfo", + name: "When module exits in ModuleStatus only, then return expected moduleTemplateInfo, with ModuleReleaseMeta", kyma: builder.NewKymaBuilder(). WithEnabledModule(testModule). WithModuleStatus(v1beta2.ModuleStatus{ @@ -636,6 +823,7 @@ func TestTemplateLookup_GetRegularTemplates_WhenModuleTemplateExists(t *testing. }, }, }).Build(), + mrmExist: true, want: templatelookup.ModuleTemplatesByModuleName{ testModule.Name: &templatelookup.ModuleTemplateInfo{ DesiredChannel: testModule.Channel, @@ -651,14 +839,25 @@ func TestTemplateLookup_GetRegularTemplates_WhenModuleTemplateExists(t *testing. for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { givenTemplateList := &v1beta2.ModuleTemplateList{} + moduleReleaseMetas := v1beta2.ModuleReleaseMetaList{} for _, module := range templatelookup.FindAvailableModules(testCase.kyma) { givenTemplateList.Items = append(givenTemplateList.Items, *builder.NewModuleTemplateBuilder(). + WithName(fmt.Sprintf("%s-%s", module.Name, testModule.Version)). WithModuleName(module.Name). WithLabelModuleName(module.Name). WithChannel(module.Channel). WithOCM(compdescv2.SchemaVersion).Build()) + if testCase.mrmExist { + moduleReleaseMetas.Items = append(moduleReleaseMetas.Items, + *builder.NewModuleReleaseMetaBuilder(). + WithModuleName(module.Name). + WithModuleChannelAndVersions([]v1beta2.ChannelVersionAssignment{ + {Channel: module.Channel, Version: testModule.Version}, + }).Build()) + } } - lookup := templatelookup.NewTemplateLookup(NewFakeModuleTemplateReader(*givenTemplateList), + lookup := templatelookup.NewTemplateLookup(NewFakeModuleTemplateReader(*givenTemplateList, + moduleReleaseMetas), provider.NewCachedDescriptorProvider()) got := lookup.GetRegularTemplates(context.TODO(), testCase.kyma) assert.Equal(t, len(got), len(testCase.want)) @@ -764,6 +963,7 @@ func TestTemplateNameMatch(t *testing.T) { func generateModuleTemplateListWithModule(moduleName, moduleChannel, moduleVersion string) v1beta2.ModuleTemplateList { templateList := v1beta2.ModuleTemplateList{} templateList.Items = append(templateList.Items, *builder.NewModuleTemplateBuilder(). + WithName(fmt.Sprintf("%s-%s", moduleName, moduleVersion)). WithModuleName(moduleName). WithLabelModuleName(moduleName). WithChannel(moduleChannel). @@ -783,6 +983,17 @@ func generateModuleTemplateListWithModule(moduleName, moduleChannel, moduleVersi return templateList } +func generateModuleReleaseMetaList(moduleName string, + channelVersions []v1beta2.ChannelVersionAssignment, +) v1beta2.ModuleReleaseMetaList { + mrmList := v1beta2.ModuleReleaseMetaList{} + mrmList.Items = append(mrmList.Items, *builder.NewModuleReleaseMetaBuilder(). + WithModuleName(moduleName). + WithModuleChannelAndVersions(channelVersions). + Build()) + return mrmList +} + type ModuleTemplateListBuilder struct { ModuleTemplates []v1beta2.ModuleTemplate } diff --git a/pkg/testutils/builder/modulereleasemeta.go b/pkg/testutils/builder/modulereleasemeta.go new file mode 100644 index 0000000000..6eb9e42e2f --- /dev/null +++ b/pkg/testutils/builder/modulereleasemeta.go @@ -0,0 +1,50 @@ +package builder + +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" +) + +type ModuleReleaseMetaBuilder struct { + moduleReleaseMeta *v1beta2.ModuleReleaseMeta +} + +func NewModuleReleaseMetaBuilder() ModuleReleaseMetaBuilder { + return ModuleReleaseMetaBuilder{ + moduleReleaseMeta: &v1beta2.ModuleReleaseMeta{ + TypeMeta: apimetav1.TypeMeta{ + APIVersion: v1beta2.GroupVersion.String(), + Kind: string(shared.ModuleReleaseMetaKind), + }, + ObjectMeta: apimetav1.ObjectMeta{ + Namespace: apimetav1.NamespaceDefault, + }, + Spec: v1beta2.ModuleReleaseMetaSpec{ + Channels: []v1beta2.ChannelVersionAssignment{}, + }, + }, + } +} + +func (m ModuleReleaseMetaBuilder) WithName(name string) ModuleReleaseMetaBuilder { + m.moduleReleaseMeta.ObjectMeta.Name = name + return m +} + +func (m ModuleReleaseMetaBuilder) WithModuleChannelAndVersions(channelVersions []v1beta2.ChannelVersionAssignment) ModuleReleaseMetaBuilder { + m.moduleReleaseMeta.Spec.Channels = append(m.moduleReleaseMeta.Spec.Channels, channelVersions...) + return m +} + +func (m ModuleReleaseMetaBuilder) WithModuleName(moduleName string) ModuleReleaseMetaBuilder { + m.moduleReleaseMeta.Spec.ModuleName = moduleName + m.moduleReleaseMeta.Name = moduleName + + return m +} + +func (m ModuleReleaseMetaBuilder) Build() *v1beta2.ModuleReleaseMeta { + return m.moduleReleaseMeta +} diff --git a/pkg/testutils/kyma.go b/pkg/testutils/kyma.go index 61ac0a9b5a..0c7f60099e 100644 --- a/pkg/testutils/kyma.go +++ b/pkg/testutils/kyma.go @@ -24,6 +24,7 @@ var ( ErrContainsUnexpectedModules = errors.New("kyma CR contains unexpected modules") ErrNotContainsExpectedModules = errors.New("kyma CR not contains expected modules") ErrModuleVersionInStatusIsIncorrect = errors.New("status.modules.version is incorrect") + ErrModuleMessageInStatusIsIncorrect = errors.New("status.modules.message is incorrect") ) func NewTestKyma(name string) *v1beta2.Kyma { @@ -357,6 +358,23 @@ func ContainsModuleInSpec(ctx context.Context, return ErrNotContainsExpectedModules } +func ModuleMessageInKymaStatusIsCorrect(ctx context.Context, clnt client.Client, + kymaName, kymaNamespace, moduleName, message string, +) error { + kyma, err := GetKyma(ctx, clnt, kymaName, kymaNamespace) + if err != nil { + return err + } + + for _, module := range kyma.Status.Modules { + if module.Name == moduleName && module.Message == message { + return nil + } + } + + return ErrModuleMessageInStatusIsIncorrect +} + func ModuleVersionInKymaStatusIsCorrect(ctx context.Context, clnt client.Client, kymaName, kymaNamespace, moduleName, moduleVersion string, ) error { diff --git a/pkg/testutils/modulereleasemeta.go b/pkg/testutils/modulereleasemeta.go new file mode 100644 index 0000000000..7d5acd7200 --- /dev/null +++ b/pkg/testutils/modulereleasemeta.go @@ -0,0 +1,60 @@ +package testutils + +import ( + "context" + "fmt" + + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/kyma-project/lifecycle-manager/api/v1beta2" + "github.com/kyma-project/lifecycle-manager/pkg/util" +) + +func UpdateChannelVersionIfModuleReleaseMetaExists(ctx context.Context, clnt client.Client, + moduleName, namespace, channel, version string, +) error { + mrm, err := GetModuleReleaseMeta(ctx, moduleName, namespace, clnt) + if err != nil { + if util.IsNotFound(err) { + return nil + } + return fmt.Errorf("get module release meta: %w", err) + } + + channelFound := false + for i, ch := range mrm.Spec.Channels { + if ch.Channel == channel { + mrm.Spec.Channels[i].Version = version + channelFound = true + break + } + } + + if !channelFound { + mrm.Spec.Channels = append(mrm.Spec.Channels, v1beta2.ChannelVersionAssignment{ + Channel: channel, + Version: version, + }) + } + + if err = clnt.Update(ctx, mrm); err != nil { + return fmt.Errorf("update module release meta: %w", err) + } + + return nil +} + +func GetModuleReleaseMeta(ctx context.Context, moduleName, namespace string, + clnt client.Client, +) (*v1beta2.ModuleReleaseMeta, error) { + mrm := &v1beta2.ModuleReleaseMeta{} + + err := clnt.Get(ctx, client.ObjectKey{ + Namespace: namespace, + Name: moduleName, + }, mrm) + if err != nil { + return nil, fmt.Errorf("get kyma: %w", err) + } + return mrm, nil +} diff --git a/pkg/testutils/moduletemplate.go b/pkg/testutils/moduletemplate.go index 3dd7bf36fa..28e7d9a587 100644 --- a/pkg/testutils/moduletemplate.go +++ b/pkg/testutils/moduletemplate.go @@ -20,7 +20,12 @@ func GetModuleTemplate(ctx context.Context, ) (*v1beta2.ModuleTemplate, error) { descriptorProvider := provider.NewCachedDescriptorProvider() templateLookup := templatelookup.NewTemplateLookup(clnt, descriptorProvider) - templateInfo := templateLookup.GetAndValidateByChannel(ctx, module.Name, module.Channel, defaultChannel) + availableModule := templatelookup.AvailableModule{ + Module: module, + } + templateInfo := templateLookup.PopulateModuleTemplateInfo(ctx, availableModule, ControlPlaneNamespace, + defaultChannel) + if templateInfo.Err != nil { return nil, fmt.Errorf("get module template: %w", templateInfo.Err) } diff --git a/tests/e2e/Makefile b/tests/e2e/Makefile index 2d8e108eee..126c8b8ebe 100644 --- a/tests/e2e/Makefile +++ b/tests/e2e/Makefile @@ -159,4 +159,7 @@ rbac-privileges: go test -timeout 20m -ginkgo.v -ginkgo.focus "RBAC Privileges" ocm-compatible-module-template: - go test -timeout 20m -ginkgo.v -ginkgo.focus "OCM Format Module Template" \ No newline at end of file + go test -timeout 20m -ginkgo.v -ginkgo.focus "OCM Format Module Template" + +modulereleasemeta-with-obsolete-moduletemplate: + go test -timeout 20m -ginkgo.v -ginkgo.focus "ModuleReleaseMeta With Obsolete ModuleTemplate" \ No newline at end of file diff --git a/tests/e2e/module_deletion_upgrade_test.go b/tests/e2e/module_deletion_upgrade_test.go index 8bbe4548d7..b097ca7a23 100644 --- a/tests/e2e/module_deletion_upgrade_test.go +++ b/tests/e2e/module_deletion_upgrade_test.go @@ -89,6 +89,12 @@ var _ = Describe("Kyma Module Upgrade Under Deletion", Ordered, func() { out, err := cmd.CombinedOutput() Expect(err).NotTo(HaveOccurred()) GinkgoWriter.Printf(string(out)) + + By("And ModuleReleaseMeta is updated if it exists") + Eventually(UpdateChannelVersionIfModuleReleaseMetaExists). + WithContext(ctx). + WithArguments(kcpClient, module.Name, ControlPlaneNamespace, v1beta2.DefaultChannel, "2.4.2-e2e-test"). + Should(Succeed()) }) It("Then Kyma Module is updated on SKR Cluster", func() { diff --git a/tests/e2e/module_upgrade_new_version_test.go b/tests/e2e/module_upgrade_new_version_test.go index beecd1a272..f8100c2a4f 100644 --- a/tests/e2e/module_upgrade_new_version_test.go +++ b/tests/e2e/module_upgrade_new_version_test.go @@ -50,6 +50,12 @@ var _ = Describe("Module Upgrade By New Version", Ordered, func() { kcpClient, newTemplateFilePath)). Should(Succeed()) + + By("And ModuleReleaseMeta is updated if it exists") + Eventually(UpdateChannelVersionIfModuleReleaseMetaExists). + WithContext(ctx). + WithArguments(kcpClient, module.Name, ControlPlaneNamespace, v1beta2.DefaultChannel, "2.4.2-e2e-test"). + Should(Succeed()) }) It("Then Module CR exists", func() { diff --git a/tests/e2e/modulereleasemeta_with_obsolete_moduletemplate_test.go b/tests/e2e/modulereleasemeta_with_obsolete_moduletemplate_test.go new file mode 100644 index 0000000000..e632abd8b9 --- /dev/null +++ b/tests/e2e/modulereleasemeta_with_obsolete_moduletemplate_test.go @@ -0,0 +1,50 @@ +package e2e_test + +import ( + "fmt" + + "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("ModuleReleaseMeta With Obsolete ModuleTemplate", Ordered, func() { + kyma := NewKymaWithSyncLabel("kyma-sample", ControlPlaneNamespace, v1beta2.DefaultChannel) + module := NewTemplateOperator(v1beta2.DefaultChannel) + + expectedErrorMessage := fmt.Sprintf("failed to get module template: ModuleTemplate.operator.kyma-project.io \"%s-%s\" not found", + module.Name, "1.0.1") + + InitEmptyKymaBeforeAll(kyma) + CleanupKymaAfterAll(kyma) + + Context("Given kyma deployed in KCP", func() { + It("When enabling Template Operator", func() { + Eventually(EnableModule). + WithContext(ctx). + WithArguments(skrClient, defaultRemoteKymaName, RemoteNamespace, module). + Should(Succeed()) + }) + + It("Then KCP Kyma CR is in \"Error\" State", func() { + Eventually(KymaIsInState). + WithContext(ctx). + WithArguments(kyma.GetName(), kyma.GetNamespace(), kcpClient, shared.StateError). + Should(Succeed()) + Consistently(KymaIsInState). + WithContext(ctx). + WithArguments(kyma.GetName(), kyma.GetNamespace(), kcpClient, shared.StateError). + Should(Succeed()) + + By("And Module Message in Kyma Status is as expected") + Eventually(ModuleMessageInKymaStatusIsCorrect). + WithContext(ctx). + WithArguments(kcpClient, kyma.GetName(), kyma.GetNamespace(), module.Name, expectedErrorMessage). + Should(Succeed()) + }) + }) +}) diff --git a/tests/e2e/rbac_privileges_test.go b/tests/e2e/rbac_privileges_test.go index ee639f3ccb..fcd1a2326e 100644 --- a/tests/e2e/rbac_privileges_test.go +++ b/tests/e2e/rbac_privileges_test.go @@ -139,6 +139,11 @@ var _ = Describe("RBAC Privileges", func() { Resources: []string{"manifests/status"}, Verbs: []string{"get", "patch", "update"}, }, + { + APIGroups: []string{"operator.kyma-project.io"}, + Resources: []string{"modulereleasemetas"}, + Verbs: []string{"get", "list"}, + }, { APIGroups: []string{"operator.kyma-project.io"}, Resources: []string{"moduletemplates"},