From e374157b3b3eb3d31b4d93328ebd26a2a120342e Mon Sep 17 00:00:00 2001 From: Tomasz Smelcerz Date: Tue, 23 Jul 2024 09:38:39 +0200 Subject: [PATCH] feat: Explicit version for module template (#1699) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit chore: Sync main to feature branch (#1718) chore: Refactor NewCachedDescriptorProvider (#1695) docs: Update KLM Local Test Setup Guide (#1680) chore: Configure different requeue intervals for Manifest reconciliation (#1690) feat: Drop multiple ways to reference modules in Kyma CR (#1672) chore: Bump k8s deps (#1703) fix: Manifest CR should update by moduletemplate generation changes (#1702) feat: Add Version to Kyma.Spec.Modules (#1694) chore: Refactor NewCachedDescriptorProvider (#1695) chore: Update branch to main (#1753) feat: Module catalog improvements implementation (#1748) feat: Remove kyma.spec.modules[].version from api (#1837) --------- Co-authored-by: Christoph Schwägerl Co-authored-by: Nesma Badr Co-authored-by: Benjamin Lindner <50365642+lindnerby@users.noreply.github.com> Co-authored-by: Tomasz Smelcerz --- .../deploy-template-operator/action.yaml | 8 + .github/workflows/test-e2e.yaml | 1 + api-version-compatibility-config.yaml | 2 + api/shared/channel.go | 14 + api/v1beta2/kyma_types.go | 46 +- api/v1beta2/moduletemplate_types.go | 51 ++ api/v1beta2/moduletemplate_types_test.go | 105 ++++ api/v1beta2/zz_generated.deepcopy.go | 16 - ...rator.kyma-project.io_moduletemplates.yaml | 11 + internal/controller/kyma/controller.go | 53 +- internal/descriptor/cache/key.go | 14 +- internal/descriptor/provider/provider_test.go | 6 +- internal/pkg/metrics/kyma.go | 1 + internal/remote/skr_context.go | 4 +- internal/remote/skr_context_test.go | 2 +- pkg/module/sync/errors.go | 5 - pkg/templatelookup/availableModules.go | 88 ++++ pkg/templatelookup/availableModules_test.go | 238 +++++++++ pkg/templatelookup/regular.go | 95 +++- pkg/templatelookup/regular_test.go | 469 +++++++++++++++++- pkg/testutils/builder/moduletemplate.go | 12 +- pkg/testutils/moduletemplate.go | 2 +- pkg/testutils/utils.go | 17 +- tests/e2e/Makefile | 4 + tests/e2e/module_install_by_version_test.go | 57 +++ tests/e2e/module_status_decoupling_test.go | 2 +- .../moduletemplate_crd_validation_test.go | 6 +- tests/integration/apiwebhook/ocm_test.go | 2 +- .../integration/controller/kcp/helper_test.go | 4 +- .../controller/kcp/remote_sync_test.go | 4 +- .../controller/kyma/helper_test.go | 10 +- .../kyma/kyma_module_channel_test.go | 9 +- .../kyma/kyma_module_enable_test.go | 45 ++ .../kyma/kyma_module_version_test.go | 40 ++ .../integration/controller/kyma/kyma_test.go | 4 +- .../controller/kyma/manifest_test.go | 8 +- .../kyma/moduletemplate_install_test.go | 2 +- .../controller/kyma/moduletemplate_test.go | 2 +- .../deletion/controller_test.go | 2 +- .../installation/controller_test.go | 2 +- .../moduletemplate/moduletemplate_test.go | 175 +++++++ .../controller/moduletemplate/suite_test.go | 111 +++++ ...e_template_operator_v2_direct_version.yaml | 78 +++ unit-test-coverage.yaml | 14 +- verification.sh | 65 +++ 45 files changed, 1729 insertions(+), 177 deletions(-) create mode 100644 api/shared/channel.go create mode 100644 api/v1beta2/moduletemplate_types_test.go delete mode 100644 pkg/module/sync/errors.go create mode 100644 pkg/templatelookup/availableModules.go create mode 100644 pkg/templatelookup/availableModules_test.go create mode 100644 tests/e2e/module_install_by_version_test.go create mode 100644 tests/integration/controller/kyma/kyma_module_enable_test.go create mode 100644 tests/integration/controller/kyma/kyma_module_version_test.go create mode 100644 tests/integration/controller/moduletemplate/moduletemplate_test.go create mode 100644 tests/integration/controller/moduletemplate/suite_test.go create mode 100755 tests/moduletemplates/moduletemplate_template_operator_v2_direct_version.yaml create mode 100755 verification.sh diff --git a/.github/actions/deploy-template-operator/action.yaml b/.github/actions/deploy-template-operator/action.yaml index 1bb9a97cbc..82c4cbba56 100644 --- a/.github/actions/deploy-template-operator/action.yaml +++ b/.github/actions/deploy-template-operator/action.yaml @@ -33,6 +33,14 @@ runs: run: | kubectl apply -f tests/moduletemplates/moduletemplate_template_operator_v2_fast.yaml kubectl apply -f tests/moduletemplates/moduletemplate_template_operator_v1_regular.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' || diff --git a/.github/workflows/test-e2e.yaml b/.github/workflows/test-e2e.yaml index 5c66a9021c..5ed84ed7e8 100644 --- a/.github/workflows/test-e2e.yaml +++ b/.github/workflows/test-e2e.yaml @@ -34,6 +34,7 @@ jobs: - module-upgrade-channel-switch - module-upgrade-new-version - unmanage-module + - module-install-by-version - skip-manifest-reconciliation - ca-certificate-rotation - self-signed-certificate-rotation diff --git a/api-version-compatibility-config.yaml b/api-version-compatibility-config.yaml index 0e8e2ebf50..a63d785f0e 100644 --- a/api-version-compatibility-config.yaml +++ b/api-version-compatibility-config.yaml @@ -21,4 +21,6 @@ operator.kyma-project.io_moduletemplates.yaml: - .spec.properties.target - .spec.required[]|select(.=="target") v1beta2: + - .spec.properties.version + - .spec.properties.moduleName - .spec.properties.customStateCheck.description diff --git a/api/shared/channel.go b/api/shared/channel.go new file mode 100644 index 0000000000..84f3b78884 --- /dev/null +++ b/api/shared/channel.go @@ -0,0 +1,14 @@ +package shared + +import "strings" + +type Channel string + +const ( + // NoneChannel when this value is defined for the ModuleTemplate, it means that the ModuleTemplate is not assigned to any channel. + NoneChannel Channel = "none" +) + +func (c Channel) Equals(value string) bool { + return string(c) == strings.ToLower(value) +} diff --git a/api/v1beta2/kyma_types.go b/api/v1beta2/kyma_types.go index ef50f8e2f8..319a80d1f5 100644 --- a/api/v1beta2/kyma_types.go +++ b/api/v1beta2/kyma_types.go @@ -74,6 +74,14 @@ type Module struct { // +kubebuilder:validation:MinLength:=3 Channel string `json:"channel,omitempty"` + // Version is the desired version of the Module. If this changes or is set, it will be used to resolve a new + // ModuleTemplate based on this specific version. + // The Version and Channel are mutually exclusive options. + // The regular expression come from here: https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string + // json:"-" to disable installation of specific versions until decided to roll this out + // see https://github.com/kyma-project/lifecycle-manager/issues/1847 + Version string `json:"-"` + // RemoteModuleTemplateRef is deprecated and will no longer have any functionality. // It will be removed in the upcoming API version. RemoteModuleTemplateRef string `json:"remoteModuleTemplateRef,omitempty"` @@ -211,14 +219,6 @@ type PartialMeta struct { const DefaultChannel = "regular" -func PartialMetaFromObject(object apimetav1.Object) PartialMeta { - return PartialMeta{ - Name: object.GetName(), - Namespace: object.GetNamespace(), - Generation: object.GetGeneration(), - } -} - func (m PartialMeta) GetName() string { return m.Name } @@ -389,36 +389,6 @@ func (kyma *Kyma) IsBeta() bool { return found && shared.IsEnabled(beta) } -type AvailableModule struct { - Module - Enabled bool - Unmanaged bool -} - -func (kyma *Kyma) GetAvailableModules() []AvailableModule { - moduleMap := make(map[string]bool) - modules := make([]AvailableModule, 0) - for _, module := range kyma.Spec.Modules { - moduleMap[module.Name] = true - modules = append(modules, AvailableModule{Module: module, Enabled: true, Unmanaged: !module.Managed}) - } - - for _, module := range kyma.Status.Modules { - _, exist := moduleMap[module.Name] - if exist { - continue - } - modules = append(modules, AvailableModule{ - Module: Module{ - Name: module.Name, - Channel: module.Channel, - }, - Enabled: false, - }) - } - return modules -} - func (kyma *Kyma) EnsureLabelsAndFinalizers() bool { if controllerutil.ContainsFinalizer(kyma, "foregroundDeletion") { return false diff --git a/api/v1beta2/moduletemplate_types.go b/api/v1beta2/moduletemplate_types.go index 413792f732..63346aa746 100644 --- a/api/v1beta2/moduletemplate_types.go +++ b/api/v1beta2/moduletemplate_types.go @@ -17,8 +17,11 @@ limitations under the License. package v1beta2 import ( + "errors" + "fmt" "strings" + "github.com/Masterminds/semver/v3" "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc" apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -78,6 +81,18 @@ type ModuleTemplateSpec struct { // +kubebuilder:validation:MinLength:=3 Channel string `json:"channel"` + // Version identifies the version of the Module. Can be empty, or a semantic version. + // +optional + // +kubebuilder:validation:Pattern:=`^((0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[a-zA-Z-][0-9a-zA-Z-]*)?)?$` + // +kubebuilder:validation:MaxLength:=32 + Version string `json:"version"` + + // ModuleName is the name of the Module. Can be empty. + // +optional + // +kubebuilder:validation:Pattern:=`^([a-z]{3,}(-[a-z]{3,})*)?$` + // +kubebuilder:validation:MaxLength:=64 + ModuleName string `json:"moduleName"` + // Mandatory indicates whether the module is mandatory. It is used to enforce the installation of the module with // its configuration in all runtime clusters. // +optional @@ -172,6 +187,42 @@ func (m *ModuleTemplate) IsInternal() bool { return false } +var ErrInvalidVersion = errors.New("can't find valid semantic version") + +// getVersionLegacy() returns the version of the ModuleTemplate from the annotation on the object. +// Remove once shared.ModuleVersionAnnotation is removed +func (m *ModuleTemplate) getVersionLegacy() (string, error) { + if m.Annotations != nil { + moduleVersion, found := m.Annotations[shared.ModuleVersionAnnotation] + if found { + return moduleVersion, nil + } + } + return "", ErrInvalidVersion +} + +// GetVersion returns the declared version of the ModuleTemplate from it's Spec. +func (m *ModuleTemplate) GetVersion() (*semver.Version, error) { + var versionValue string + var err error + + if m.Spec.Version == "" { + versionValue, err = m.getVersionLegacy() + if err != nil { + return nil, err + } + } else { + versionValue = m.Spec.Version + + } + + version, err := semver.NewVersion(versionValue) + if err != nil { + return nil, fmt.Errorf("%w: %s", ErrInvalidVersion, err.Error()) + } + return version, nil +} + func (m *ModuleTemplate) IsBeta() bool { if isBeta, found := m.Labels[shared.BetaLabel]; found { return strings.ToLower(isBeta) == shared.EnableLabelValue diff --git a/api/v1beta2/moduletemplate_types_test.go b/api/v1beta2/moduletemplate_types_test.go new file mode 100644 index 0000000000..89e01014f9 --- /dev/null +++ b/api/v1beta2/moduletemplate_types_test.go @@ -0,0 +1,105 @@ +package v1beta2 + +import ( + "strings" + "testing" + + "github.com/kyma-project/lifecycle-manager/api/shared" + apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func Test_GetVersion(t *testing.T) { + const testVersion = "1.0.1" + const otherVersion = "0.0.1" + tests := []struct { + name string + m *ModuleTemplate + expectedVersion string + expectedErr string + }{ + { + name: "Test GetVersion() by annotation (legacy)", + m: &ModuleTemplate{ + ObjectMeta: apimetav1.ObjectMeta{ + Annotations: map[string]string{ + shared.ModuleVersionAnnotation: testVersion, + }, + }, + }, + expectedVersion: testVersion, + }, + { + name: "Test GetVersion() by explicit version in Spec", + m: &ModuleTemplate{ + Spec: ModuleTemplateSpec{ + Version: testVersion, + }, + }, + expectedVersion: testVersion, + }, + { + name: "Test GetVersion() with both version in Spec and annotation", + m: &ModuleTemplate{ + ObjectMeta: apimetav1.ObjectMeta{ + Annotations: map[string]string{ + shared.ModuleVersionAnnotation: otherVersion, + }, + }, + Spec: ModuleTemplateSpec{ + Version: testVersion, + }, + }, + expectedVersion: testVersion, + }, + { + name: "Test GetVersion without any version info", + m: &ModuleTemplate{ + ObjectMeta: apimetav1.ObjectMeta{ + Annotations: map[string]string{}, + }, + Spec: ModuleTemplateSpec{}, + }, + expectedErr: ErrInvalidVersion.Error(), + }, + { + name: "Test GetVersion with invalid version", + m: &ModuleTemplate{ + Spec: ModuleTemplateSpec{ + Version: "invalid", + }, + }, + expectedErr: "Invalid Semantic Version", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + tt := tt + actualVersion, err := tt.m.GetVersion() + if err != nil { + if actualVersion != nil { + t.Errorf("GetVersion(): Returned version should be nil when error is not nil") + } + if tt.expectedErr == "" { + t.Errorf("GetVersion(): Unexpected error: %v", err) + } + if !strings.Contains(err.Error(), tt.expectedErr) { + t.Errorf("GetVersion(): Actual error = %v, expected error: %v", err, tt.expectedErr) + } + return + } + + if actualVersion == nil { + t.Errorf("GetVersion(): Returned version should not be nil when error is nil") + } + + if tt.expectedVersion == "" { + t.Errorf("GetVersion(): Expected version is empty but non-nil version is returned") + } + + if actualVersion.String() != tt.expectedVersion { + t.Errorf("GetVersion(): actual version = %v, expected version: %v", actualVersion.String(), tt.expectedVersion) + } + }) + } +} diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index c7376ac24d..f9a21664d2 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -25,22 +25,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AvailableModule) DeepCopyInto(out *AvailableModule) { - *out = *in - out.Module = in.Module -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AvailableModule. -func (in *AvailableModule) DeepCopy() *AvailableModule { - if in == nil { - return nil - } - out := new(AvailableModule) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CustomStateCheck) DeepCopyInto(out *CustomStateCheck) { *out = *in diff --git a/config/crd/bases/operator.kyma-project.io_moduletemplates.yaml b/config/crd/bases/operator.kyma-project.io_moduletemplates.yaml index dde836326b..d3af7c209b 100644 --- a/config/crd/bases/operator.kyma-project.io_moduletemplates.yaml +++ b/config/crd/bases/operator.kyma-project.io_moduletemplates.yaml @@ -225,6 +225,17 @@ spec: Mandatory indicates whether the module is mandatory. It is used to enforce the installation of the module with its configuration in all runtime clusters. type: boolean + moduleName: + description: ModuleName is the name of the Module. Can be empty. + maxLength: 64 + pattern: ^([a-z]{3,}(-[a-z]{3,})*)?$ + type: string + version: + description: Version identifies the version of the Module. Can be + empty, or a semantic version. + maxLength: 32 + pattern: ^((0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[a-zA-Z-][0-9a-zA-Z-]*)?)?$ + type: string required: - channel - descriptor diff --git a/internal/controller/kyma/controller.go b/internal/controller/kyma/controller.go index 5af6ef98e7..e59b75484b 100644 --- a/internal/controller/kyma/controller.go +++ b/internal/controller/kyma/controller.go @@ -34,11 +34,10 @@ import ( "github.com/kyma-project/lifecycle-manager/api/v1beta2" "github.com/kyma-project/lifecycle-manager/internal/descriptor/provider" "github.com/kyma-project/lifecycle-manager/internal/event" - "github.com/kyma-project/lifecycle-manager/internal/manifest/parser" + parse "github.com/kyma-project/lifecycle-manager/internal/manifest/parser" "github.com/kyma-project/lifecycle-manager/internal/pkg/metrics" "github.com/kyma-project/lifecycle-manager/internal/remote" "github.com/kyma-project/lifecycle-manager/pkg/log" - "github.com/kyma-project/lifecycle-manager/pkg/module/common" "github.com/kyma-project/lifecycle-manager/pkg/module/sync" "github.com/kyma-project/lifecycle-manager/pkg/queue" "github.com/kyma-project/lifecycle-manager/pkg/status" @@ -47,14 +46,16 @@ import ( "github.com/kyma-project/lifecycle-manager/pkg/watcher" ) -var ErrManifestsStillExist = errors.New("manifests still exist") +var ( + ErrManifestsStillExist = errors.New("manifests still exist") + ErrInvalidKymaSpec = errors.New("invalid kyma spec") +) const ( - moduleReconciliationError event.Reason = "ModuleReconciliationError" - metricsError event.Reason = "MetricsError" - updateSpecError event.Reason = "UpdateSpecError" - updateStatusError event.Reason = "UpdateStatusError" - patchStatusError event.Reason = "PatchStatus" + metricsError event.Reason = "MetricsError" + updateSpecError event.Reason = "UpdateSpecError" + updateStatusError event.Reason = "UpdateStatusError" + patchStatusError event.Reason = "PatchStatus" ) type Reconciler struct { @@ -266,6 +267,14 @@ func (r *Reconciler) syncStatusToRemote(ctx context.Context, kcpKyma *v1beta2.Ky return nil } +// ValidateDefaultChannel validates the Kyma spec. +func (r *Reconciler) ValidateDefaultChannel(kyma *v1beta2.Kyma) error { + if shared.NoneChannel.Equals(kyma.Spec.Channel) { + return fmt.Errorf("%w: value \"none\" is not allowed in spec.channel", ErrInvalidKymaSpec) + } + return nil +} + // replaceSpecFromRemote replaces the spec from control-lane Kyma with the remote Kyma spec as single source of truth. func (r *Reconciler) replaceSpecFromRemote(ctx context.Context, controlPlaneKyma *v1beta2.Kyma) error { remoteKyma, err := r.fetchRemoteKyma(ctx, controlPlaneKyma) @@ -276,7 +285,12 @@ func (r *Reconciler) replaceSpecFromRemote(ctx context.Context, controlPlaneKyma } return err } - remote.ReplaceModules(controlPlaneKyma, remoteKyma) + + remote.ReplaceSpec(controlPlaneKyma, remoteKyma) + + if err := r.ValidateDefaultChannel(controlPlaneKyma); err != nil { + return err + } return nil } @@ -475,17 +489,14 @@ func (r *Reconciler) updateKyma(ctx context.Context, kyma *v1beta2.Kyma) error { } func (r *Reconciler) reconcileManifests(ctx context.Context, kyma *v1beta2.Kyma) error { - modules, err := r.GenerateModulesFromTemplate(ctx, kyma) - if err != nil { - return fmt.Errorf("error while fetching modules during processing: %w", err) - } + templates := templatelookup.NewTemplateLookup(client.Reader(r), r.DescriptorProvider).GetRegularTemplates(ctx, kyma) + parser := parse.NewParser(r.Client, r.DescriptorProvider, r.InKCPMode, r.RemoteSyncNamespace) + modules := parser.GenerateModulesFromTemplates(kyma, templates) runner := sync.New(r) - if err := runner.ReconcileManifests(ctx, kyma, modules); err != nil { return fmt.Errorf("sync failed: %w", err) } - runner.SyncModuleStatus(ctx, kyma, modules, r.Metrics) // If module get removed from kyma, the module deletion happens here. if err := r.DeleteNoLongerExistingModules(ctx, kyma); err != nil { @@ -532,18 +543,6 @@ func (r *Reconciler) updateStatusWithError(ctx context.Context, kyma *v1beta2.Ky return nil } -func (r *Reconciler) GenerateModulesFromTemplate(ctx context.Context, kyma *v1beta2.Kyma) (common.Modules, error) { - lookup := templatelookup.NewTemplateLookup(client.Reader(r), r.DescriptorProvider) - templates := lookup.GetRegularTemplates(ctx, kyma) - for _, template := range templates { - if template.Err != nil { - r.Event.Warning(kyma, moduleReconciliationError, template.Err) - } - } - parser := parser.NewParser(r.Client, r.DescriptorProvider, r.InKCPMode, r.RemoteSyncNamespace) - return parser.GenerateModulesFromTemplates(kyma, templates), nil -} - func (r *Reconciler) DeleteNoLongerExistingModules(ctx context.Context, kyma *v1beta2.Kyma) error { moduleStatus := kyma.GetNoLongerExistingModuleStatus() var err error diff --git a/internal/descriptor/cache/key.go b/internal/descriptor/cache/key.go index 643dee5742..f5d7cf90e2 100644 --- a/internal/descriptor/cache/key.go +++ b/internal/descriptor/cache/key.go @@ -3,22 +3,16 @@ package cache import ( "fmt" - "github.com/Masterminds/semver/v3" - - "github.com/kyma-project/lifecycle-manager/api/shared" "github.com/kyma-project/lifecycle-manager/api/v1beta2" ) type DescriptorKey string func GenerateDescriptorKey(template *v1beta2.ModuleTemplate) DescriptorKey { - if template.Annotations != nil { - moduleVersion := template.Annotations[shared.ModuleVersionAnnotation] - _, err := semver.NewVersion(moduleVersion) - if moduleVersion != "" && err == nil { - return DescriptorKey(fmt.Sprintf("%s:%s:%d:%s", template.Name, template.Spec.Channel, template.Generation, - moduleVersion)) - } + version, err := template.GetVersion() + if err == nil { + return DescriptorKey(fmt.Sprintf("%s:%s:%d:%s", template.Name, template.Spec.Channel, template.Generation, + version)) } return DescriptorKey(fmt.Sprintf("%s:%s:%d", template.Name, template.Spec.Channel, template.Generation)) diff --git a/internal/descriptor/provider/provider_test.go b/internal/descriptor/provider/provider_test.go index b526e35bf0..22fe5d97ec 100644 --- a/internal/descriptor/provider/provider_test.go +++ b/internal/descriptor/provider/provider_test.go @@ -14,7 +14,7 @@ import ( ) func TestGetDescriptor_OnEmptySpec_ReturnsErrDecode(t *testing.T) { - descriptorProvider := provider.NewCachedDescriptorProvider() + descriptorProvider := provider.NewCachedDescriptorProvider() // assuming it handles nil cache internally template := &v1beta2.ModuleTemplate{} _, err := descriptorProvider.GetDescriptor(template) @@ -81,7 +81,9 @@ func TestAdd_OnDescriptorTypeButNull_ReturnsNoError(t *testing.T) { func TestGetDescriptor_OnEmptyCache_AddsDescriptorFromTemplate(t *testing.T) { descriptorCache := cache.NewDescriptorCache() - descriptorProvider := provider.CachedDescriptorProvider{DescriptorCache: descriptorCache} + descriptorProvider := &provider.CachedDescriptorProvider{ + DescriptorCache: descriptorCache, + } expected := &v1beta2.Descriptor{ ComponentDescriptor: &compdesc.ComponentDescriptor{ diff --git a/internal/pkg/metrics/kyma.go b/internal/pkg/metrics/kyma.go index f65d0584fb..6d95a23cf2 100644 --- a/internal/pkg/metrics/kyma.go +++ b/internal/pkg/metrics/kyma.go @@ -51,6 +51,7 @@ const ( KymaDeletion KymaRequeueReason = "kyma_deletion" KymaRetrieval KymaRequeueReason = "kyma_retrieval" KymaUnauthorized KymaRequeueReason = "kyma_unauthorized" + SpecValidation KymaRequeueReason = "spec_validation" ) func NewKymaMetrics(sharedMetrics *SharedMetrics) *KymaMetrics { diff --git a/internal/remote/skr_context.go b/internal/remote/skr_context.go index 492043fa4e..5c8dc095f9 100644 --- a/internal/remote/skr_context.go +++ b/internal/remote/skr_context.go @@ -180,8 +180,8 @@ func (s *SkrContext) SynchronizeKyma(ctx context.Context, kcpKyma, remoteKyma *v return nil } -// ReplaceModules replaces modules specification from control plane Kyma with Remote Kyma specifications. -func ReplaceModules(controlPlaneKyma *v1beta2.Kyma, remoteKyma *v1beta2.Kyma) { +// ReplaceSpec replaces 'spec' attributes in control plane Kyma with values from Remote Kyma. +func ReplaceSpec(controlPlaneKyma *v1beta2.Kyma, remoteKyma *v1beta2.Kyma) { controlPlaneKyma.Spec.Modules = []v1beta2.Module{} controlPlaneKyma.Spec.Modules = append(controlPlaneKyma.Spec.Modules, remoteKyma.Spec.Modules...) controlPlaneKyma.Spec.Channel = remoteKyma.Spec.Channel diff --git a/internal/remote/skr_context_test.go b/internal/remote/skr_context_test.go index 0ef62e1347..af037ae945 100644 --- a/internal/remote/skr_context_test.go +++ b/internal/remote/skr_context_test.go @@ -59,7 +59,7 @@ func TestReplaceWithVirtualKyma(t *testing.T) { t.Parallel() kcpKyma := createKyma(testCase.kcpKyma.channel, testCase.kcpKyma.modules) remoteKyma := createKyma(testCase.remoteKyma.channel, testCase.remoteKyma.modules) - remote.ReplaceModules(kcpKyma, remoteKyma) + remote.ReplaceSpec(kcpKyma, remoteKyma) assert.Equal(t, testCase.expectedKyma.channel, kcpKyma.Spec.Channel) var virtualModules []string for _, module := range kcpKyma.Spec.Modules { diff --git a/pkg/module/sync/errors.go b/pkg/module/sync/errors.go deleted file mode 100644 index 623885d2fb..0000000000 --- a/pkg/module/sync/errors.go +++ /dev/null @@ -1,5 +0,0 @@ -package sync - -import "errors" - -var ErrManifestConversion = errors.New("manifest casting error") diff --git a/pkg/templatelookup/availableModules.go b/pkg/templatelookup/availableModules.go new file mode 100644 index 0000000000..2f22eec488 --- /dev/null +++ b/pkg/templatelookup/availableModules.go @@ -0,0 +1,88 @@ +package templatelookup + +import ( + "errors" + "fmt" + + "github.com/kyma-project/lifecycle-manager/api/shared" + "github.com/kyma-project/lifecycle-manager/api/v1beta2" +) + +var ( + ErrInvalidModuleInSpec = errors.New("invalid configuration in Kyma spec.modules") + ErrInvalidModuleInStatus = errors.New("invalid module entry in Kyma status") +) + +type AvailableModule struct { + v1beta2.Module + Enabled bool + ValidationError error + Unmanaged bool +} + +func (a AvailableModule) IsInstalledByVersion() bool { + return a.configuredWithVersionInSpec() || a.installedwithVersionInStatus() +} + +// configuredWithVersionInSpec returns true if the Module is enabled in Spec using a specific version instead of a channel. +func (a AvailableModule) configuredWithVersionInSpec() bool { + return a.Enabled && a.Version != "" && a.Channel == "" +} + +// installedwithVersionInStatus returns true if the Module installed using a specific version (instead of a channel) is reported in Status. +func (a AvailableModule) installedwithVersionInStatus() bool { + return !a.Enabled && shared.NoneChannel.Equals(a.Channel) && a.Version != "" +} + +// FindAvailableModules returns a list of AvailableModule objects based on the Kyma CR Spec and Status. +func FindAvailableModules(kyma *v1beta2.Kyma) []AvailableModule { + moduleMap := make(map[string]bool) + modules := make([]AvailableModule, 0) + for _, module := range kyma.Spec.Modules { + moduleMap[module.Name] = true + if shared.NoneChannel.Equals(module.Channel) { + modules = append(modules, AvailableModule{ + Module: module, + Enabled: true, + ValidationError: fmt.Errorf("%w for module %s: Channel \"none\" is not allowed", ErrInvalidModuleInSpec, module.Name), + Unmanaged: !module.Managed, + }) + continue + } + if module.Version != "" && module.Channel != "" { + modules = append(modules, AvailableModule{ + Module: module, + Enabled: true, + ValidationError: fmt.Errorf("%w for module %s: Version and channel are mutually exclusive options", ErrInvalidModuleInSpec, module.Name), + Unmanaged: !module.Managed, + }) + continue + } + modules = append(modules, AvailableModule{Module: module, Enabled: true, Unmanaged: !module.Managed}) + } + + for _, moduleInStatus := range kyma.Status.Modules { + _, exist := moduleMap[moduleInStatus.Name] + if exist { + continue + } + + modules = append(modules, AvailableModule{ + Module: v1beta2.Module{ + Name: moduleInStatus.Name, + Channel: moduleInStatus.Channel, + Version: moduleInStatus.Version, + }, + Enabled: false, + ValidationError: determineModuleValidity(moduleInStatus), + }) + } + return modules +} + +func determineModuleValidity(moduleStatus v1beta2.ModuleStatus) error { + if moduleStatus.Template == nil { + return fmt.Errorf("%w for module %s: ModuleTemplate reference is missing", ErrInvalidModuleInStatus, moduleStatus.Name) + } + return nil +} diff --git a/pkg/templatelookup/availableModules_test.go b/pkg/templatelookup/availableModules_test.go new file mode 100644 index 0000000000..16bb46b704 --- /dev/null +++ b/pkg/templatelookup/availableModules_test.go @@ -0,0 +1,238 @@ +package templatelookup_test + +import ( + "errors" + "strings" + "testing" + + apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kyma-project/lifecycle-manager/api/v1beta2" + "github.com/kyma-project/lifecycle-manager/pkg/templatelookup" +) + +func Test_GetAvailableModules_When_ModuleInSpecOnly(t *testing.T) { + tests := []struct { + name string + KymaSpec v1beta2.KymaSpec + want []availableModuleDescription + }{ + { + name: "When Channel \"none\" is used, then the module is invalid", + KymaSpec: v1beta2.KymaSpec{ + Modules: []v1beta2.Module{ + {Name: "Module1", Channel: "none"}, + }, + }, + want: []availableModuleDescription{ + { + Module: v1beta2.Module{Name: "Module1", Channel: "none"}, + Enabled: true, + ValidationErrorContains: "Channel \"none\" is not allowed", ExpectedError: templatelookup.ErrInvalidModuleInSpec, + }, + }, + }, + { + name: "When Channel and Version are both set, then the module is invalid", + KymaSpec: v1beta2.KymaSpec{ + Modules: []v1beta2.Module{ + {Name: "Module1", Channel: "regular", Version: "v1.0"}, + }, + }, + want: []availableModuleDescription{ + { + Module: v1beta2.Module{Name: "Module1", Channel: "regular", Version: "v1.0"}, + Enabled: true, + ValidationErrorContains: "Version and channel are mutually exclusive options", ExpectedError: templatelookup.ErrInvalidModuleInSpec, + }, + }, + }, + { + name: "When Channel is set, then the module is valid", + KymaSpec: v1beta2.KymaSpec{ + Modules: []v1beta2.Module{ + {Name: "Module1", Channel: "regular"}, + }, + }, + want: []availableModuleDescription{ + {Module: v1beta2.Module{Name: "Module1", Channel: "regular"}, Enabled: true}, + }, + }, + { + name: "When Version is set, then the module is valid", + KymaSpec: v1beta2.KymaSpec{ + Modules: []v1beta2.Module{ + {Name: "Module1", Version: "v1.0"}, + }, + }, + want: []availableModuleDescription{ + {Module: v1beta2.Module{Name: "Module1", Version: "v1.0"}, Enabled: true}, + }, + }, + } + for ti := range tests { + testcase := tests[ti] + t.Run(testcase.name, func(t *testing.T) { + kyma := &v1beta2.Kyma{ + Spec: testcase.KymaSpec, + } + + got := templatelookup.FindAvailableModules(kyma) + if len(got) != len(testcase.want) { + t.Errorf("GetAvailableModules() = %v, want %v", got, testcase.want) + } + for gi := range got { + if !testcase.want[gi].Equals(got[gi]) { + t.Errorf("GetAvailableModules() = %v, want %v", got, testcase.want) + } + } + }) + } +} + +func Test_GetAvailableModules_When_ModuleInStatusOnly(t *testing.T) { + tests := []struct { + name string + KymaStatus v1beta2.KymaStatus + want []availableModuleDescription + }{ + { + name: "When Template exists, then the module is valid", + KymaStatus: v1beta2.KymaStatus{ + Modules: []v1beta2.ModuleStatus{ + { + Name: "Module1", + Channel: "regular", + Version: "v1.0", + Template: &v1beta2.TrackingObject{TypeMeta: apimetav1.TypeMeta{Kind: "ModuleTemplate"}}, + }, + }, + }, + want: []availableModuleDescription{ + {Module: v1beta2.Module{Name: "Module1", Channel: "regular", Version: "v1.0"}, Enabled: false}, + }, + }, + { + name: "When Template not exists,then the module is invalid", + KymaStatus: v1beta2.KymaStatus{ + Modules: []v1beta2.ModuleStatus{ + { + Name: "Module1", + Channel: "regular", + Version: "v1.0", + Template: nil, + }, + }, + }, + want: []availableModuleDescription{ + { + Module: v1beta2.Module{Name: "Module1", Channel: "regular", Version: "v1.0"}, + Enabled: false, + ValidationErrorContains: "ModuleTemplate reference is missing", ExpectedError: templatelookup.ErrInvalidModuleInStatus, + }, + }, + }, + } + for ti := range tests { + testcase := tests[ti] + t.Run(testcase.name, func(t *testing.T) { + kyma := &v1beta2.Kyma{ + Status: testcase.KymaStatus, + } + + got := templatelookup.FindAvailableModules(kyma) + if len(got) != len(testcase.want) { + t.Errorf("GetAvailableModules() = %v, want %v", got, testcase.want) + } + for gi := range got { + if !testcase.want[gi].Equals(got[gi]) { + t.Errorf("GetAvailableModules() = %v, want %v", got, testcase.want) + } + } + }) + } +} + +func Test_GetAvailableModules_When_ModuleExistsInSpecAndStatus(t *testing.T) { + tests := []struct { + name string + KymaSpec v1beta2.KymaSpec + KymaStatus v1beta2.KymaStatus + want []availableModuleDescription + }{ + { + name: "When Module have different version between Spec and Status, the output should be based on Spec", + KymaSpec: v1beta2.KymaSpec{ + Modules: []v1beta2.Module{ + {Name: "Module1", Version: "v1.1"}, + }, + }, + KymaStatus: v1beta2.KymaStatus{ + Modules: []v1beta2.ModuleStatus{ + { + Name: "Module1", + Version: "v1.0", + }, + }, + }, + want: []availableModuleDescription{ + {Module: v1beta2.Module{Name: "Module1", Version: "v1.1"}, Enabled: true}, + }, + }, + } + for ti := range tests { + testcase := tests[ti] + t.Run(testcase.name, func(t *testing.T) { + kyma := &v1beta2.Kyma{ + Spec: testcase.KymaSpec, + Status: testcase.KymaStatus, + } + got := templatelookup.FindAvailableModules(kyma) + if len(got) != len(testcase.want) { + t.Errorf("GetAvailableModules() = %v, want %v", got, testcase.want) + } + for gi := range got { + if !testcase.want[gi].Equals(got[gi]) { + t.Errorf("GetAvailableModules() = %v, want %v", got, testcase.want) + } + } + }) + } +} + +type availableModuleDescription struct { + Module v1beta2.Module + Enabled bool + ValidationErrorContains string + ExpectedError error +} + +func (amd availableModuleDescription) Equals(other templatelookup.AvailableModule) bool { + if amd.Module.Name != other.Name { + return false + } + if amd.Module.Channel != other.Channel { + return false + } + if amd.Module.Version != other.Version { + return false + } + if amd.Enabled != other.Enabled { + return false + } + if amd.ExpectedError != nil && other.ValidationError == nil { + return false + } + if amd.ExpectedError == nil && other.ValidationError != nil { + return false + } + if amd.ExpectedError != nil && other.ValidationError != nil { + if !errors.Is(other.ValidationError, amd.ExpectedError) { + return false + } + if !strings.Contains(other.ValidationError.Error(), amd.ValidationErrorContains) { + return false + } + } + return true +} diff --git a/pkg/templatelookup/regular.go b/pkg/templatelookup/regular.go index 0f3ee7a1e5..4a24a64c61 100644 --- a/pkg/templatelookup/regular.go +++ b/pkg/templatelookup/regular.go @@ -46,12 +46,24 @@ type ModuleTemplatesByModuleName map[string]*ModuleTemplateInfo func (t *TemplateLookup) GetRegularTemplates(ctx context.Context, kyma *v1beta2.Kyma) ModuleTemplatesByModuleName { templates := make(ModuleTemplatesByModuleName) - for _, module := range kyma.GetAvailableModules() { + for _, module := range FindAvailableModules(kyma) { _, found := templates[module.Name] if found { continue } - templateInfo := t.GetAndValidate(ctx, module.Name, module.Channel, kyma.Spec.Channel) + if module.ValidationError != nil { + templates[module.Name] = &ModuleTemplateInfo{Err: module.ValidationError} + 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 = ValidateTemplateMode(templateInfo, kyma) if templateInfo.Err != nil { templates[module.Name] = &templateInfo @@ -94,13 +106,13 @@ func ValidateTemplateMode(template ModuleTemplateInfo, kyma *v1beta2.Kyma) Modul return template } -func (t *TemplateLookup) GetAndValidate(ctx context.Context, name, channel, defaultChannel string) ModuleTemplateInfo { +func (t *TemplateLookup) GetAndValidateByChannel(ctx context.Context, name, channel, defaultChannel string) ModuleTemplateInfo { desiredChannel := getDesiredChannel(channel, defaultChannel) info := ModuleTemplateInfo{ DesiredChannel: desiredChannel, } - template, err := t.getTemplate(ctx, name, desiredChannel) + template, err := t.getTemplateByChannel(ctx, name, desiredChannel) if err != nil { info.Err = err return info @@ -120,6 +132,20 @@ func (t *TemplateLookup) GetAndValidate(ctx context.Context, name, channel, defa return info } +func (t *TemplateLookup) GetAndValidateByVersion(ctx context.Context, name, version string) ModuleTemplateInfo { + info := ModuleTemplateInfo{ + DesiredChannel: string(shared.NoneChannel), + } + template, err := t.getTemplateByVersion(ctx, name, version) + if err != nil { + info.Err = err + return info + } + + info.ModuleTemplate = template + return info +} + func logUsedChannel(ctx context.Context, name string, actualChannel string, defaultChannel string) { logger := logf.FromContext(ctx) if actualChannel != defaultChannel { @@ -163,10 +189,9 @@ func markInvalidChannelSkewUpdate(ctx context.Context, moduleTemplateInfo *Modul "previousTemplateChannel", moduleStatus.Channel, ) - if moduleTemplateInfo.Spec.Channel == moduleStatus.Channel { + if moduleTemplateInfo.Spec.Channel == moduleStatus.Channel && moduleTemplateInfo.Spec.Channel != string(shared.NoneChannel) { return } - checkLog.Info("outdated ModuleTemplate: channel skew") versionInTemplate, err := semver.NewVersion(templateVersion) @@ -221,7 +246,7 @@ func getDesiredChannel(moduleChannel, globalChannel string) string { return desiredChannel } -func (t *TemplateLookup) getTemplate(ctx context.Context, name, desiredChannel string) ( +func (t *TemplateLookup) getTemplateByChannel(ctx context.Context, name, desiredChannel string) ( *v1beta2.ModuleTemplate, error, ) { templateList := &v1beta2.ModuleTemplateList{} @@ -232,26 +257,72 @@ func (t *TemplateLookup) getTemplate(ctx context.Context, name, desiredChannel s var filteredTemplates []*v1beta2.ModuleTemplate for _, template := range templateList.Items { - if template.Labels[shared.ModuleName] == name && template.Spec.Channel == desiredChannel { + if TemplateNameMatch(&template, name) && template.Spec.Channel == desiredChannel { filteredTemplates = append(filteredTemplates, &template) continue } } + if len(filteredTemplates) > 1 { + return nil, NewMoreThanOneTemplateCandidateErr(name, templateList.Items) + } + + if len(filteredTemplates) == 0 { + return nil, fmt.Errorf("%w: for module %s in channel %s ", + ErrNoTemplatesInListResult, name, desiredChannel) + } + + if filteredTemplates[0].Spec.Mandatory { + return nil, fmt.Errorf("%w: for module %s in channel %s", + ErrTemplateMarkedAsMandatory, name, desiredChannel) + } + + return filteredTemplates[0], nil +} + +func (t *TemplateLookup) getTemplateByVersion(ctx context.Context, name, version string) ( + *v1beta2.ModuleTemplate, error, +) { + templateList := &v1beta2.ModuleTemplateList{} + err := t.List(ctx, templateList) + if err != nil { + return nil, fmt.Errorf("failed to list module templates on lookup: %w", err) + } + + var filteredTemplates []*v1beta2.ModuleTemplate + for _, template := range templateList.Items { + template := template + if TemplateNameMatch(&template, name) && shared.NoneChannel.Equals(template.Spec.Channel) && template.Spec.Version == version { + filteredTemplates = append(filteredTemplates, &template) + continue + } + } if len(filteredTemplates) > 1 { return nil, NewMoreThanOneTemplateCandidateErr(name, templateList.Items) } if len(filteredTemplates) == 0 { - return nil, fmt.Errorf("%w: in channel %s for module %s", - ErrNoTemplatesInListResult, desiredChannel, name) + return nil, fmt.Errorf("%w: for module %s in version %s", + ErrNoTemplatesInListResult, name, version) } if filteredTemplates[0].Spec.Mandatory { - return nil, fmt.Errorf("%w: in channel %s for module %s", - ErrTemplateMarkedAsMandatory, desiredChannel, name) + return nil, fmt.Errorf("%w: for module %s in version %s", + ErrTemplateMarkedAsMandatory, name, version) } return filteredTemplates[0], nil } +func TemplateNameMatch(template *v1beta2.ModuleTemplate, name string) bool { + if len(template.Spec.ModuleName) > 0 { + return template.Spec.ModuleName == name + } + + // Drop the legacyCondition once the label 'shared.ModuleName' is removed: https://github.com/kyma-project/lifecycle-manager/issues/1796 + if template.Labels == nil { + return false + } + return template.Labels[shared.ModuleName] == name +} + func NewMoreThanOneTemplateCandidateErr(moduleName string, candidateTemplates []v1beta2.ModuleTemplate, ) error { diff --git a/pkg/templatelookup/regular_test.go b/pkg/templatelookup/regular_test.go index 03170c4310..3ce08511b7 100644 --- a/pkg/templatelookup/regular_test.go +++ b/pkg/templatelookup/regular_test.go @@ -10,6 +10,7 @@ import ( compdescv2 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/versions/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/kyma-project/lifecycle-manager/api/shared" @@ -20,6 +21,14 @@ import ( "github.com/kyma-project/lifecycle-manager/pkg/testutils/builder" ) +const ( + version1 = "1.0.1" + version2 = "2.2.0" + version3 = "3.0.3" + + versionUpgradeErr = "as a higher version (" + version3 + ") of the module was previously installed" +) + type FakeModuleTemplateReader struct { templateList v1beta2.ModuleTemplateList } @@ -92,6 +101,56 @@ func TestValidateTemplateMode(t *testing.T) { } } +func Test_GetRegularTemplates_WhenInvalidModuleProvided(t *testing.T) { + tests := []struct { + name string + KymaSpec v1beta2.KymaSpec + KymaStatus v1beta2.KymaStatus + wantErr error + }{ + { + name: "When Module in Spec contains both Channel and Version, Then result contains error", + KymaSpec: v1beta2.KymaSpec{ + Modules: []v1beta2.Module{ + {Name: "Module1", Channel: "regular", Version: "v1.0"}, + }, + }, + wantErr: templatelookup.ErrInvalidModuleInSpec, + }, + { + name: "When Template not exists in Status, Then result contains error", + KymaStatus: v1beta2.KymaStatus{ + Modules: []v1beta2.ModuleStatus{ + { + Name: "Module1", + Channel: "regular", + Version: "v1.0", + Template: nil, + }, + }, + }, + wantErr: templatelookup.ErrInvalidModuleInStatus, + }, + } + + for _, tt := range tests { + test := tt + t.Run(tt.name, func(t *testing.T) { + lookup := templatelookup.NewTemplateLookup(nil, provider.NewCachedDescriptorProvider()) + kyma := &v1beta2.Kyma{ + Spec: test.KymaSpec, + Status: test.KymaStatus, + } + got := lookup.GetRegularTemplates(context.TODO(), kyma) + for _, err := range got { + if !errors.Is(err.Err, test.wantErr) { + t.Errorf("GetRegularTemplates() = %v, want %v", got, test.wantErr) + } + } + }) + } +} + func TestTemplateLookup_GetRegularTemplates_WhenSwitchModuleChannel(t *testing.T) { testModule := testutils.NewTestModule("module1", "new_channel") @@ -108,14 +167,14 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchModuleChannel(t *testing.T WithModuleStatus(v1beta2.ModuleStatus{ Name: testModule.Name, Channel: v1beta2.DefaultChannel, - Version: "1.0.0", + Version: version1, Template: &v1beta2.TrackingObject{ PartialMeta: v1beta2.PartialMeta{ Generation: 1, }, }, }).Build(), - availableModuleTemplate: generateModuleTemplateListWithModule(testModule.Name, testModule.Channel, "1.1.0"), + availableModuleTemplate: generateModuleTemplateListWithModule(testModule.Name, testModule.Channel, version2), want: templatelookup.ModuleTemplatesByModuleName{ testModule.Name: &templatelookup.ModuleTemplateInfo{ DesiredChannel: testModule.Channel, @@ -129,14 +188,14 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchModuleChannel(t *testing.T WithModuleStatus(v1beta2.ModuleStatus{ Name: testModule.Name, Channel: v1beta2.DefaultChannel, - Version: "1.1.0", + Version: version2, Template: &v1beta2.TrackingObject{ PartialMeta: v1beta2.PartialMeta{ Generation: 1, }, }, }).Build(), - availableModuleTemplate: generateModuleTemplateListWithModule(testModule.Name, testModule.Channel, "1.0.0"), + availableModuleTemplate: generateModuleTemplateListWithModule(testModule.Name, testModule.Channel, version1), want: templatelookup.ModuleTemplatesByModuleName{ testModule.Name: &templatelookup.ModuleTemplateInfo{ DesiredChannel: testModule.Channel, @@ -162,24 +221,255 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchModuleChannel(t *testing.T } } -func generateModuleTemplateListWithModule(moduleName, moduleChannel, moduleVersion string) v1beta2.ModuleTemplateList { - templateList := v1beta2.ModuleTemplateList{} - templateList.Items = append(templateList.Items, *builder.NewModuleTemplateBuilder(). - WithModuleName(moduleName). - WithChannel(moduleChannel). - WithDescriptor(&v1beta2.Descriptor{ - ComponentDescriptor: &compdesc.ComponentDescriptor{ - Metadata: compdesc.Metadata{ - ConfiguredVersion: compdescv2.SchemaVersion, - }, - ComponentSpec: compdesc.ComponentSpec{ - ObjectMeta: ocmmetav1.ObjectMeta{ - Version: moduleVersion, +func TestTemplateLookup_GetRegularTemplates_WhenSwitchBetweenModuleVersions(t *testing.T) { + moduleToInstall := moduleToInstallByVersion("module1", version2) + + availableModuleTemplates := (&ModuleTemplateListBuilder{}). + Add(moduleToInstall.Name, "regular", version1). + Add(moduleToInstall.Name, "fast", version2). + Add(moduleToInstall.Name, "experimental", version3). + Add(moduleToInstall.Name, string(shared.NoneChannel), version1). + Add(moduleToInstall.Name, string(shared.NoneChannel), version2). + Add(moduleToInstall.Name, string(shared.NoneChannel), version3). + Build() + + tests := []struct { + name string + kyma *v1beta2.Kyma + wantVersion string + wantChannel string + wantErrContains string + }{ + { + name: "When upgrade version, then result contains no error", + kyma: builder.NewKymaBuilder(). + WithEnabledModule(moduleToInstall). + WithModuleStatus(v1beta2.ModuleStatus{ + Name: moduleToInstall.Name, + Channel: string(shared.NoneChannel), + Version: version1, + Template: &v1beta2.TrackingObject{ + PartialMeta: v1beta2.PartialMeta{ + Generation: 1, + }, }, - }, - }, - }).Build()) - return templateList + }).Build(), + wantChannel: string(shared.NoneChannel), + wantVersion: version2, + }, + { + name: "When downgrade version, then result contains error", + kyma: builder.NewKymaBuilder(). + WithEnabledModule(moduleToInstall). + WithModuleStatus(v1beta2.ModuleStatus{ + Name: moduleToInstall.Name, + Channel: string(shared.NoneChannel), + Version: version3, + Template: &v1beta2.TrackingObject{ + PartialMeta: v1beta2.PartialMeta{ + Generation: 1, + }, + }, + }).Build(), + wantErrContains: versionUpgradeErr, + }, + } + + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + lookup := templatelookup.NewTemplateLookup(NewFakeModuleTemplateReader(availableModuleTemplates), + provider.NewCachedDescriptorProvider()) + got := lookup.GetRegularTemplates(context.TODO(), testCase.kyma) + assert.Len(t, got, 1) + for key, module := range got { + assert.Equal(t, key, moduleToInstall.Name) + if testCase.wantErrContains != "" { + assert.Contains(t, module.Err.Error(), testCase.wantErrContains) + } else { + assert.Equal(t, testCase.wantChannel, module.DesiredChannel) + assert.Equal(t, testCase.wantVersion, module.ModuleTemplate.Spec.Version) + } + } + }) + } +} + +func TestTemplateLookup_GetRegularTemplates_WhenSwitchFromChannelToVersion(t *testing.T) { + moduleToInstall := moduleToInstallByVersion("module1", version2) + availableModuleTemplates := (&ModuleTemplateListBuilder{}). + Add(moduleToInstall.Name, "regular", version1). + Add(moduleToInstall.Name, "fast", version2). + Add(moduleToInstall.Name, "experimental", version3). + Add(moduleToInstall.Name, string(shared.NoneChannel), version1). + Add(moduleToInstall.Name, string(shared.NoneChannel), version2). + Add(moduleToInstall.Name, string(shared.NoneChannel), version3). + Build() + + tests := []struct { + name string + kyma *v1beta2.Kyma + wantVersion string + wantChannel string + wantErrContains string + }{ + { + name: "When staying with the same version, then result contains no error", + kyma: builder.NewKymaBuilder(). + WithEnabledModule(moduleToInstall). + WithModuleStatus(v1beta2.ModuleStatus{ + Name: moduleToInstall.Name, + Channel: "fast", + Version: version2, + Template: &v1beta2.TrackingObject{ + PartialMeta: v1beta2.PartialMeta{ + Generation: 1, + }, + }, + }).Build(), + wantChannel: string(shared.NoneChannel), + wantVersion: version2, + }, + { + name: "When upgrade version, then result contains no error", + kyma: builder.NewKymaBuilder(). + WithEnabledModule(moduleToInstall). + WithModuleStatus(v1beta2.ModuleStatus{ + Name: moduleToInstall.Name, + Channel: "regular", + Version: version1, + Template: &v1beta2.TrackingObject{ + PartialMeta: v1beta2.PartialMeta{ + Generation: 1, + }, + }, + }).Build(), + wantChannel: string(shared.NoneChannel), + wantVersion: version2, + }, + { + name: "When downgrade version, then result contains error", + kyma: builder.NewKymaBuilder(). + WithEnabledModule(moduleToInstall). + WithModuleStatus(v1beta2.ModuleStatus{ + Name: moduleToInstall.Name, + Channel: "experimental", + Version: version3, + Template: &v1beta2.TrackingObject{ + PartialMeta: v1beta2.PartialMeta{ + Generation: 1, + }, + }, + }).Build(), + wantErrContains: versionUpgradeErr, + }, + } + + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + lookup := templatelookup.NewTemplateLookup(NewFakeModuleTemplateReader(availableModuleTemplates), + provider.NewCachedDescriptorProvider()) + got := lookup.GetRegularTemplates(context.TODO(), testCase.kyma) + assert.Len(t, got, 1) + for key, module := range got { + assert.Equal(t, key, moduleToInstall.Name) + if testCase.wantErrContains != "" { + assert.Contains(t, module.Err.Error(), testCase.wantErrContains) + } else { + assert.Equal(t, testCase.wantChannel, module.DesiredChannel) + assert.Equal(t, testCase.wantVersion, module.ModuleTemplate.Spec.Version) + } + } + }) + } +} + +func TestTemplateLookup_GetRegularTemplates_WhenSwitchFromVersionToChannel(t *testing.T) { + moduleToInstall := testutils.NewTestModule("module1", "new_channel") + availableModuleTemplates := (&ModuleTemplateListBuilder{}). + Add(moduleToInstall.Name, "regular", version1). + Add(moduleToInstall.Name, "new_channel", version2). + Add(moduleToInstall.Name, "fast", version3). + Add(moduleToInstall.Name, string(shared.NoneChannel), version1). + Add(moduleToInstall.Name, string(shared.NoneChannel), version2). + Add(moduleToInstall.Name, string(shared.NoneChannel), version3). + Build() + + tests := []struct { + name string + kyma *v1beta2.Kyma + wantVersion string + wantChannel string + wantErrContains string + }{ + { + name: "When staying with the same version, then result contains no error", + kyma: builder.NewKymaBuilder(). + WithEnabledModule(moduleToInstall). + WithModuleStatus(v1beta2.ModuleStatus{ + Name: moduleToInstall.Name, + Channel: string(shared.NoneChannel), + Version: version2, + Template: &v1beta2.TrackingObject{ + PartialMeta: v1beta2.PartialMeta{ + Generation: 1, + }, + }, + }).Build(), + wantChannel: "new_channel", + wantVersion: version2, + }, + { + name: "When upgrade version, then result contains no error", + kyma: builder.NewKymaBuilder(). + WithEnabledModule(moduleToInstall). + WithModuleStatus(v1beta2.ModuleStatus{ + Name: moduleToInstall.Name, + Channel: string(shared.NoneChannel), + Version: version1, + Template: &v1beta2.TrackingObject{ + PartialMeta: v1beta2.PartialMeta{ + Generation: 1, + }, + }, + }).Build(), + wantChannel: "new_channel", + wantVersion: version2, + }, + { + name: "When downgrade version, then result contains error", + kyma: builder.NewKymaBuilder(). + WithEnabledModule(moduleToInstall). + WithModuleStatus(v1beta2.ModuleStatus{ + Name: moduleToInstall.Name, + Channel: string(shared.NoneChannel), + Version: version3, + Template: &v1beta2.TrackingObject{ + PartialMeta: v1beta2.PartialMeta{ + Generation: 1, + }, + }, + }).Build(), + wantErrContains: versionUpgradeErr, + }, + } + + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + lookup := templatelookup.NewTemplateLookup(NewFakeModuleTemplateReader(availableModuleTemplates), + provider.NewCachedDescriptorProvider()) + got := lookup.GetRegularTemplates(context.TODO(), testCase.kyma) + assert.Len(t, got, 1) + for key, module := range got { + assert.Equal(t, key, moduleToInstall.Name) + if testCase.wantErrContains != "" { + assert.Contains(t, module.Err.Error(), testCase.wantErrContains) + } else { + assert.Equal(t, testCase.wantChannel, module.DesiredChannel) + assert.Equal(t, testCase.wantVersion, module.ModuleTemplate.Spec.Version) + } + } + }) + } } func TestNewTemplateLookup_GetRegularTemplates_WhenModuleTemplateContainsInvalidDescriptor(t *testing.T) { @@ -224,9 +514,10 @@ func TestNewTemplateLookup_GetRegularTemplates_WhenModuleTemplateContainsInvalid for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { givenTemplateList := &v1beta2.ModuleTemplateList{} - for _, module := range testCase.kyma.GetAvailableModules() { + for _, module := range templatelookup.FindAvailableModules(testCase.kyma) { givenTemplateList.Items = append(givenTemplateList.Items, *builder.NewModuleTemplateBuilder(). WithModuleName(module.Name). + WithLabelModuleName(module.Name). WithChannel(module.Channel). WithDescriptor(nil). WithRawDescriptor([]byte("{invalid_json}")).Build()) @@ -358,9 +649,10 @@ func TestTemplateLookup_GetRegularTemplates_WhenModuleTemplateExists(t *testing. for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { givenTemplateList := &v1beta2.ModuleTemplateList{} - for _, module := range testCase.kyma.GetAvailableModules() { + for _, module := range templatelookup.FindAvailableModules(testCase.kyma) { givenTemplateList.Items = append(givenTemplateList.Items, *builder.NewModuleTemplateBuilder(). WithModuleName(module.Name). + WithLabelModuleName(module.Name). WithChannel(module.Channel). WithOCM(compdescv2.SchemaVersion).Build()) } @@ -378,3 +670,134 @@ func TestTemplateLookup_GetRegularTemplates_WhenModuleTemplateExists(t *testing. }) } } + +func TestTemplateNameMatch(t *testing.T) { + targetName := "module1" + + tests := []struct { + name string + template v1beta2.ModuleTemplate + want bool + }{ + { + name: "When moduleName is empty and no labels, Then return false", + template: v1beta2.ModuleTemplate{ + Spec: v1beta2.ModuleTemplateSpec{ + ModuleName: "", + }, + }, + want: false, + }, + { + name: "When moduleName is not equal to target name, Then return false", + template: v1beta2.ModuleTemplate{ + Spec: v1beta2.ModuleTemplateSpec{ + ModuleName: "module2", + }, + }, + want: false, + }, + { + name: "When moduleName is equal to target name, Then return true", + template: v1beta2.ModuleTemplate{ + Spec: v1beta2.ModuleTemplateSpec{ + ModuleName: "module1", + }, + }, + want: true, + }, + { + name: "When moduleName is empty but legacy label matches, Then return true", + template: v1beta2.ModuleTemplate{ + ObjectMeta: apimetav1.ObjectMeta{ + Labels: map[string]string{ + shared.ModuleName: "module1", + }, + }, + Spec: v1beta2.ModuleTemplateSpec{ + ModuleName: "", + }, + }, + want: true, + }, + { + name: "When moduleName does not match and legacy label matches, Then return false as moduleName takes precedence over label", + template: v1beta2.ModuleTemplate{ + ObjectMeta: apimetav1.ObjectMeta{ + Labels: map[string]string{ + shared.ModuleName: "module1", + }, + }, + Spec: v1beta2.ModuleTemplateSpec{ + ModuleName: "module2", + }, + }, + want: false, + }, + { + name: "When moduleName does matches and legacy label does not match, Then return true as moduleName takes precedence over label", + template: v1beta2.ModuleTemplate{ + ObjectMeta: apimetav1.ObjectMeta{ + Labels: map[string]string{ + shared.ModuleName: "module2", + }, + }, + Spec: v1beta2.ModuleTemplateSpec{ + ModuleName: "module1", + }, + }, + want: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + if got := templatelookup.TemplateNameMatch(&tt.template, targetName); got != tt.want { + assert.Equal(t, tt.want, got) + } + }) + } +} + +func generateModuleTemplateListWithModule(moduleName, moduleChannel, moduleVersion string) v1beta2.ModuleTemplateList { + templateList := v1beta2.ModuleTemplateList{} + templateList.Items = append(templateList.Items, *builder.NewModuleTemplateBuilder(). + WithModuleName(moduleName). + WithLabelModuleName(moduleName). + WithChannel(moduleChannel). + WithVersion(moduleVersion). + WithDescriptor(&v1beta2.Descriptor{ + ComponentDescriptor: &compdesc.ComponentDescriptor{ + Metadata: compdesc.Metadata{ + ConfiguredVersion: compdescv2.SchemaVersion, + }, + ComponentSpec: compdesc.ComponentSpec{ + ObjectMeta: ocmmetav1.ObjectMeta{ + Version: moduleVersion, + }, + }, + }, + }).Build()) + return templateList +} + +type ModuleTemplateListBuilder struct { + ModuleTemplates []v1beta2.ModuleTemplate +} + +func (mtlb *ModuleTemplateListBuilder) Add(moduleName, moduleChannel, moduleVersion string) *ModuleTemplateListBuilder { + list := generateModuleTemplateListWithModule(moduleName, moduleChannel, moduleVersion) + mtlb.ModuleTemplates = append(mtlb.ModuleTemplates, list.Items...) + return mtlb +} + +func (mtlb *ModuleTemplateListBuilder) Build() v1beta2.ModuleTemplateList { + return v1beta2.ModuleTemplateList{ + Items: mtlb.ModuleTemplates, + } +} + +func moduleToInstallByVersion(moduleName, moduleVersion string) v1beta2.Module { + return testutils.NewTestModuleWithChannelVersion(moduleName, "", moduleVersion) +} diff --git a/pkg/testutils/builder/moduletemplate.go b/pkg/testutils/builder/moduletemplate.go index 9bcea4b576..da8ed4e265 100644 --- a/pkg/testutils/builder/moduletemplate.go +++ b/pkg/testutils/builder/moduletemplate.go @@ -56,12 +56,22 @@ func (m ModuleTemplateBuilder) WithName(name string) ModuleTemplateBuilder { return m } +func (m ModuleTemplateBuilder) WithVersion(version string) ModuleTemplateBuilder { + m.moduleTemplate.Spec.Version = version + return m +} + +func (m ModuleTemplateBuilder) WithModuleName(moduleName string) ModuleTemplateBuilder { + m.moduleTemplate.Spec.ModuleName = moduleName + return m +} + func (m ModuleTemplateBuilder) WithGeneration(generation int) ModuleTemplateBuilder { m.moduleTemplate.ObjectMeta.Generation = int64(generation) return m } -func (m ModuleTemplateBuilder) WithModuleName(moduleName string) ModuleTemplateBuilder { +func (m ModuleTemplateBuilder) WithLabelModuleName(moduleName string) ModuleTemplateBuilder { if m.moduleTemplate.Labels == nil { m.moduleTemplate.Labels = make(map[string]string) } diff --git a/pkg/testutils/moduletemplate.go b/pkg/testutils/moduletemplate.go index b820dc36f9..3dd7bf36fa 100644 --- a/pkg/testutils/moduletemplate.go +++ b/pkg/testutils/moduletemplate.go @@ -20,7 +20,7 @@ func GetModuleTemplate(ctx context.Context, ) (*v1beta2.ModuleTemplate, error) { descriptorProvider := provider.NewCachedDescriptorProvider() templateLookup := templatelookup.NewTemplateLookup(clnt, descriptorProvider) - templateInfo := templateLookup.GetAndValidate(ctx, module.Name, module.Channel, defaultChannel) + templateInfo := templateLookup.GetAndValidateByChannel(ctx, module.Name, module.Channel, defaultChannel) if templateInfo.Err != nil { return nil, fmt.Errorf("get module template: %w", templateInfo.Err) } diff --git a/pkg/testutils/utils.go b/pkg/testutils/utils.go index 076a3ea265..dbd2f1533b 100644 --- a/pkg/testutils/utils.go +++ b/pkg/testutils/utils.go @@ -28,7 +28,7 @@ import ( const ( defaultBufferSize = 2048 - Timeout = time.Second * 40 + Timeout = time.Second * 10 ConsistentCheckTimeout = time.Second * 10 Interval = time.Millisecond * 250 ) @@ -42,18 +42,27 @@ var ( ) func NewTestModule(name, channel string) v1beta2.Module { - return NewTestModuleWithFixName(fmt.Sprintf("%s-%s", name, random.Name()), channel) + return NewTestModuleWithFixName(fmt.Sprintf("%s-%s", name, random.Name()), channel, "") +} + +func NewTestModuleWithChannelVersion(name, channel, version string) v1beta2.Module { + return NewTestModuleWithFixName(fmt.Sprintf("%s-%s", name, random.Name()), channel, version) +} + +func NewTemplateOperatorWithVersion(version string) v1beta2.Module { + return NewTestModuleWithFixName("template-operator", "", version) } func NewTemplateOperator(channel string) v1beta2.Module { - return NewTestModuleWithFixName("template-operator", channel) + return NewTestModuleWithFixName("template-operator", channel, "") } -func NewTestModuleWithFixName(name, channel string) v1beta2.Module { +func NewTestModuleWithFixName(name, channel, version string) v1beta2.Module { return v1beta2.Module{ Name: name, Channel: channel, Managed: true, + Version: version, } } diff --git a/tests/e2e/Makefile b/tests/e2e/Makefile index ac1231cfe9..2d8e108eee 100644 --- a/tests/e2e/Makefile +++ b/tests/e2e/Makefile @@ -84,6 +84,7 @@ test: kyma-deprovision-with-foreground-propagation \ purge-metrics \ module-upgrade-channel-switch \ module-upgrade-new-version \ + module-install-by-version \ ca-certificate-rotation \ self-signed-certificate-rotation \ misconfigured-kyma-secret @@ -142,6 +143,9 @@ module-upgrade-channel-switch: module-upgrade-new-version: go test -timeout 20m -ginkgo.v -ginkgo.focus "Module Upgrade By New Version" +module-install-by-version: + go test -timeout 20m -ginkgo.v -ginkgo.focus "Module Install By Version" + ca-certificate-rotation: go test -timeout 20m -ginkgo.v -ginkgo.focus "CA Certificate Rotation" diff --git a/tests/e2e/module_install_by_version_test.go b/tests/e2e/module_install_by_version_test.go new file mode 100644 index 0000000000..01bc104457 --- /dev/null +++ b/tests/e2e/module_install_by_version_test.go @@ -0,0 +1,57 @@ +package e2e_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/kyma-project/lifecycle-manager/api/shared" + "github.com/kyma-project/lifecycle-manager/api/v1beta2" + . "github.com/kyma-project/lifecycle-manager/pkg/testutils" + "github.com/kyma-project/lifecycle-manager/pkg/testutils/builder" +) + +var _ = Describe("Module Install By Version", Ordered, func() { + kyma := NewKymaWithSyncLabel("kyma-sample", ControlPlaneNamespace, v1beta2.DefaultChannel) + templateOperatorModule := NewTemplateOperatorWithVersion("2.4.2-e2e-test") + moduleCR := builder.NewModuleCRBuilder(). + WithName("sample-2.4.2-e2e-test"). + WithNamespace(RemoteNamespace).Build() + + InitEmptyKymaBeforeAll(kyma) + CleanupKymaAfterAll(kyma) + + Context("Given SKR Cluster", func() { + It("When Template-Operator Module is enabled on SKR Kyma CR in a specific version", func() { + Skip("Version attribute is disabled for now on the CRD level") + Eventually(EnableModule). + WithContext(ctx). + WithArguments(runtimeClient, defaultRemoteKymaName, RemoteNamespace, templateOperatorModule). + Should(Succeed()) + }) + + It("Then Module CR exists", func() { + Skip("Version attribute is disabled for now on the CRD level") + Eventually(ModuleCRExists). + WithContext(ctx). + WithArguments(runtimeClient, moduleCR). + Should(Succeed()) + + By("And Module Operator Deployment exists") + Eventually(DeploymentIsReady). + WithContext(ctx). + WithArguments(runtimeClient, ModuleDeploymentNameInNewerVersion, TestModuleResourceNamespace). + Should(Succeed()) + + By("And KCP Kyma CR is in \"Ready\" State") + Eventually(KymaIsInState). + WithContext(ctx). + WithArguments(kyma.GetName(), kyma.GetNamespace(), controlPlaneClient, shared.StateReady). + Should(Succeed()) + By("And Module is in \"Ready\" State") + Eventually(CheckModuleState). + WithContext(ctx). + WithArguments(controlPlaneClient, kyma.GetName(), kyma.GetNamespace(), templateOperatorModule.Name, shared.StateReady). + Should(Succeed()) + }) + }) +}) diff --git a/tests/e2e/module_status_decoupling_test.go b/tests/e2e/module_status_decoupling_test.go index e6acf686ed..457e474ea4 100644 --- a/tests/e2e/module_status_decoupling_test.go +++ b/tests/e2e/module_status_decoupling_test.go @@ -25,7 +25,7 @@ const ( func RunModuleStatusDecouplingTest(resourceKind ResourceKind) { kyma := NewKymaWithSyncLabel("kyma-sample", ControlPlaneNamespace, v1beta2.DefaultChannel) module := NewTemplateOperator(v1beta2.DefaultChannel) - moduleWrongConfig := NewTestModuleWithFixName("template-operator-misconfigured", "regular") + moduleWrongConfig := NewTestModuleWithFixName("template-operator-misconfigured", "regular", "") moduleCR := NewTestModuleCR(RemoteNamespace) InitEmptyKymaBeforeAll(kyma) CleanupKymaAfterAll(kyma) diff --git a/tests/integration/apiwebhook/moduletemplate_crd_validation_test.go b/tests/integration/apiwebhook/moduletemplate_crd_validation_test.go index 843c13ab3c..00a05d2a8c 100644 --- a/tests/integration/apiwebhook/moduletemplate_crd_validation_test.go +++ b/tests/integration/apiwebhook/moduletemplate_crd_validation_test.go @@ -39,7 +39,7 @@ var _ = Describe("Webhook ValidationCreate Strict", Ordered, func() { WithArguments(crd).Should(Succeed()) template := builder.NewModuleTemplateBuilder(). - WithModuleName("test-module"). + WithLabelModuleName("test-module"). WithModuleCR(&data). WithChannel(v1beta2.DefaultChannel). WithOCM(compdescv2.SchemaVersion).Build() @@ -56,7 +56,7 @@ var _ = Describe("Webhook ValidationCreate Strict", Ordered, func() { WithContext(webhookServerContext). WithArguments(crd).Should(Succeed()) template := builder.NewModuleTemplateBuilder(). - WithModuleName("test-module"). + WithLabelModuleName("test-module"). WithModuleCR(&data). WithChannel(v1beta2.DefaultChannel). WithOCM(compdescv2.SchemaVersion).Build() @@ -72,7 +72,7 @@ var _ = Describe("Webhook ValidationCreate Strict", Ordered, func() { WithContext(webhookServerContext). WithArguments(crd).Should(Succeed()) template := builder.NewModuleTemplateBuilder(). - WithModuleName("test-module"). + WithLabelModuleName("test-module"). WithModuleCR(&data). WithChannel(v1beta2.DefaultChannel). WithOCM(compdescv2.SchemaVersion).Build() diff --git a/tests/integration/apiwebhook/ocm_test.go b/tests/integration/apiwebhook/ocm_test.go index 420ff2d6ba..b00cc65216 100644 --- a/tests/integration/apiwebhook/ocm_test.go +++ b/tests/integration/apiwebhook/ocm_test.go @@ -31,7 +31,7 @@ var _ = Describe( WithContext(webhookServerContext). WithArguments(crd).Should(Succeed()) template := builder.NewModuleTemplateBuilder(). - WithModuleName("test-module"). + WithLabelModuleName("test-module"). WithChannel(v1beta2.DefaultChannel). WithModuleCR(&data). WithOCM(v3alpha1.SchemaVersion).Build() diff --git a/tests/integration/controller/kcp/helper_test.go b/tests/integration/controller/kcp/helper_test.go index d2a1135e24..acd4abdbce 100644 --- a/tests/integration/controller/kcp/helper_test.go +++ b/tests/integration/controller/kcp/helper_test.go @@ -56,7 +56,7 @@ func registerControlPlaneLifecycleForKyma(kyma *v1beta2.Kyma) { func DeleteModuleTemplates(ctx context.Context, kcpClient client.Client, kyma *v1beta2.Kyma) { for _, module := range kyma.Spec.Modules { template := builder.NewModuleTemplateBuilder(). - WithModuleName(module.Name). + WithLabelModuleName(module.Name). WithChannel(module.Channel). WithOCM(compdescv2.SchemaVersion).Build() Eventually(DeleteCR, Timeout, Interval). @@ -68,7 +68,7 @@ func DeleteModuleTemplates(ctx context.Context, kcpClient client.Client, kyma *v func DeployModuleTemplates(ctx context.Context, kcpClient client.Client, kyma *v1beta2.Kyma) { for _, module := range kyma.Spec.Modules { template := builder.NewModuleTemplateBuilder(). - WithModuleName(module.Name). + WithLabelModuleName(module.Name). WithChannel(module.Channel). WithOCM(compdescv2.SchemaVersion).Build() Eventually(kcpClient.Create, Timeout, Interval).WithContext(ctx). diff --git a/tests/integration/controller/kcp/remote_sync_test.go b/tests/integration/controller/kcp/remote_sync_test.go index d41447dd9f..cc3e355465 100644 --- a/tests/integration/controller/kcp/remote_sync_test.go +++ b/tests/integration/controller/kcp/remote_sync_test.go @@ -35,12 +35,12 @@ var _ = Describe("Kyma sync into Remote Cluster", Ordered, func() { moduleInKCP := NewTestModule("in-kcp", v1beta2.DefaultChannel) defaultCR := builder.NewModuleCRBuilder().WithSpec(InitSpecKey, InitSpecValue).Build() SKRTemplate := builder.NewModuleTemplateBuilder(). - WithModuleName(moduleInSKR.Name). + WithLabelModuleName(moduleInSKR.Name). WithChannel(moduleInSKR.Channel). WithModuleCR(defaultCR). WithOCM(compdescv2.SchemaVersion).Build() KCPTemplate := builder.NewModuleTemplateBuilder(). - WithModuleName(moduleInKCP.Name). + WithLabelModuleName(moduleInKCP.Name). WithChannel(moduleInKCP.Channel). WithModuleCR(defaultCR). WithOCM(compdescv2.SchemaVersion).Build() diff --git a/tests/integration/controller/kyma/helper_test.go b/tests/integration/controller/kyma/helper_test.go index e94443a3b5..4a1dd4ee10 100644 --- a/tests/integration/controller/kyma/helper_test.go +++ b/tests/integration/controller/kyma/helper_test.go @@ -63,7 +63,7 @@ func DeleteModuleTemplates(ctx context.Context, kcpClient client.Client, kyma *v for _, module := range kyma.Spec.Modules { template := builder.NewModuleTemplateBuilder(). WithName(createModuleTemplateName(module)). - WithModuleName(module.Name). + WithLabelModuleName(module.Name). WithChannel(module.Channel). WithOCM(compdescv2.SchemaVersion).Build() Eventually(DeleteCR, Timeout, Interval). @@ -76,11 +76,11 @@ func DeployModuleTemplates(ctx context.Context, kcpClient client.Client, kyma *v for _, module := range kyma.Spec.Modules { template := builder.NewModuleTemplateBuilder(). WithName(createModuleTemplateName(module)). - WithModuleName(module.Name). + WithLabelModuleName(module.Name). WithChannel(module.Channel). WithOCM(compdescv2.SchemaVersion).Build() - Eventually(kcpClient.Create, Timeout, Interval).WithContext(ctx). - WithArguments(template). + Eventually(CreateCR, Timeout, Interval).WithContext(ctx). + WithArguments(kcpClient, template). Should(Succeed()) } } @@ -106,7 +106,7 @@ func createModuleTemplateName(module v1beta2.Module) string { func newMandatoryModuleTemplate() *v1beta2.ModuleTemplate { return builder.NewModuleTemplateBuilder(). WithName("mandatory-template"). - WithModuleName("mandatory-template-operator"). + WithLabelModuleName("mandatory-template-operator"). WithChannel(mandatoryChannel). WithMandatory(true). WithOCM(compdescv2.SchemaVersion).Build() diff --git a/tests/integration/controller/kyma/kyma_module_channel_test.go b/tests/integration/controller/kyma/kyma_module_channel_test.go index 916101ad6d..bd4abde1c4 100644 --- a/tests/integration/controller/kyma/kyma_module_channel_test.go +++ b/tests/integration/controller/kyma/kyma_module_channel_test.go @@ -22,6 +22,7 @@ import ( const ( FastChannel = "fast" ValidChannel = "valid" + InvalidNoneChannel = string(shared.NoneChannel) InValidChannel = "Invalid01" // lower case characters from a to z InValidMinLengthChannel = "ch" // minlength = 3 InValidMaxLengthChannel = "averylongchannelwhichlargerthanallowedmaxlength" // maxlength = 32 @@ -77,7 +78,7 @@ var _ = Describe("module channel different from the global channel", func() { }) }) -var _ = Describe("Given invalid channel", func() { +var _ = Describe("Given invalid channel which is rejected by CRD validation rules", func() { DescribeTable( "Test kyma CR, module template creation", func(givenCondition func() error) { Eventually(givenCondition, Timeout, Interval).Should(Succeed()) @@ -280,7 +281,7 @@ func CleanupModuleTemplateSetsForKyma(kyma *v1beta2.Kyma) func() { for _, module := range kyma.Spec.Modules { template := builder.NewModuleTemplateBuilder(). WithName(fmt.Sprintf("%s-%s", module.Name, v1beta2.DefaultChannel)). - WithModuleName(module.Name). + WithLabelModuleName(module.Name). WithChannel(module.Channel). WithOCM(compdescv2.SchemaVersion).Build() Eventually(DeleteCR, Timeout, Interval). @@ -291,7 +292,7 @@ func CleanupModuleTemplateSetsForKyma(kyma *v1beta2.Kyma) func() { for _, module := range kyma.Spec.Modules { template := builder.NewModuleTemplateBuilder(). WithName(fmt.Sprintf("%s-%s", module.Name, FastChannel)). - WithModuleName(module.Name). + WithLabelModuleName(module.Name). WithChannel(module.Channel). WithOCM(compdescv2.SchemaVersion).Build() Eventually(DeleteCR, Timeout, Interval). @@ -385,7 +386,7 @@ func whenUpdatingEveryModuleChannel(kymaName, kymaNamespace, channel string) fun func createModuleTemplateSetsForKyma(modules []v1beta2.Module, modifiedVersion, channel string) error { for _, module := range modules { template := builder.NewModuleTemplateBuilder(). - WithModuleName(module.Name). + WithLabelModuleName(module.Name). WithChannel(module.Channel). WithOCM(compdescv2.SchemaVersion).Build() diff --git a/tests/integration/controller/kyma/kyma_module_enable_test.go b/tests/integration/controller/kyma/kyma_module_enable_test.go new file mode 100644 index 0000000000..29da999b45 --- /dev/null +++ b/tests/integration/controller/kyma/kyma_module_enable_test.go @@ -0,0 +1,45 @@ +package kyma_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/kyma-project/lifecycle-manager/api/shared" + "github.com/kyma-project/lifecycle-manager/api/v1beta2" + . "github.com/kyma-project/lifecycle-manager/pkg/testutils" +) + +var _ = Describe("Given kyma CR with invalid module enabled", Ordered, func() { + kyma := NewTestKyma("kyma") + BeforeAll(func() { + Eventually(CreateCR). + WithContext(ctx). + WithArguments(kcpClient, kyma).Should(Succeed()) + }) + + It("When enable module with channel and version, expect module status in Error state", func() { + Skip("Version attribute is disabled for now on the CRD level") + module := NewTestModuleWithChannelVersion("test", v1beta2.DefaultChannel, "1.0.0") + Eventually(givenKymaWithModule, Timeout, Interval). + WithArguments(kyma, module).Should(Succeed()) + Eventually(expectKymaStatusModules(ctx, kyma, module.Name, shared.StateError), Timeout, + Interval).Should(Succeed()) + }) + It("When enable module with none channel, expect module status become error", func() { + module := NewTestModuleWithChannelVersion("test", string(shared.NoneChannel), "") + Eventually(givenKymaWithModule, Timeout, Interval). + WithArguments(kyma, module).Should(Succeed()) + Eventually(expectKymaStatusModules(ctx, kyma, module.Name, shared.StateError), Timeout, + Interval).Should(Succeed()) + }) +}) + +func givenKymaWithModule(kyma *v1beta2.Kyma, module v1beta2.Module) error { + if err := EnableModule(ctx, kcpClient, kyma.Name, kyma.Namespace, module); err != nil { + return err + } + Eventually(SyncKyma, Timeout, Interval). + WithContext(ctx).WithArguments(kcpClient, kyma).Should(Succeed()) + DeployModuleTemplates(ctx, kcpClient, kyma) + return nil +} diff --git a/tests/integration/controller/kyma/kyma_module_version_test.go b/tests/integration/controller/kyma/kyma_module_version_test.go new file mode 100644 index 0000000000..a1cc7c4bfe --- /dev/null +++ b/tests/integration/controller/kyma/kyma_module_version_test.go @@ -0,0 +1,40 @@ +package kyma_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/kyma-project/lifecycle-manager/api/v1beta2" + + . "github.com/kyma-project/lifecycle-manager/pkg/testutils" +) + +var _ = Describe("Given invalid module version which is rejected by CRD validation rules", func() { + DescribeTable( + "Test enable module", func(givenCondition func() error) { + Skip("Version attribute is disabled for now on the CRD level") + Eventually(givenCondition, Timeout, Interval).Should(Succeed()) + }, + + Entry( + "invalid semantic version", + givenKymaWithInvalidModuleVersion("20240101"), + ), + Entry( + "invalid semantic version", + givenKymaWithInvalidModuleVersion("1.0.0.abc"), + ), + ) +}) + +func givenKymaWithInvalidModuleVersion(version string) func() error { + return func() error { + kyma := NewTestKyma("kyma") + kyma.Spec.Channel = v1beta2.DefaultChannel + module := NewTestModuleWithChannelVersion("test", v1beta2.DefaultChannel, version) + kyma.Spec.Modules = append( + kyma.Spec.Modules, module) + err := kcpClient.Create(ctx, kyma) + return ignoreInvalidError(err) + } +} diff --git a/tests/integration/controller/kyma/kyma_test.go b/tests/integration/controller/kyma/kyma_test.go index 711fd90b90..3c89082133 100644 --- a/tests/integration/controller/kyma/kyma_test.go +++ b/tests/integration/controller/kyma/kyma_test.go @@ -375,7 +375,7 @@ var _ = Describe("Kyma skip Reconciliation", Ordered, func() { It("Should deploy ModuleTemplate", func() { data := builder.NewModuleCRBuilder().WithSpec(InitSpecKey, InitSpecValue).Build() template := builder.NewModuleTemplateBuilder(). - WithModuleName(module.Name). + WithLabelModuleName(module.Name). WithChannel(module.Channel). WithModuleCR(data). WithOCM(compdescv2.SchemaVersion). @@ -465,7 +465,7 @@ var _ = Describe("Kyma.Spec.Status.Modules.Resource.Namespace should be empty fo It("Should deploy ModuleTemplate", func() { for _, module := range kyma.Spec.Modules { template := builder.NewModuleTemplateBuilder(). - WithModuleName(module.Name). + WithLabelModuleName(module.Name). WithChannel(module.Channel). WithOCM(compdescv2.SchemaVersion). WithAnnotation(shared.IsClusterScopedAnnotation, shared.EnableLabelValue).Build() diff --git a/tests/integration/controller/kyma/manifest_test.go b/tests/integration/controller/kyma/manifest_test.go index 6a0f986e0b..ad3f887d7e 100644 --- a/tests/integration/controller/kyma/manifest_test.go +++ b/tests/integration/controller/kyma/manifest_test.go @@ -23,9 +23,10 @@ import ( "github.com/kyma-project/lifecycle-manager/pkg/templatelookup" "github.com/kyma-project/lifecycle-manager/pkg/testutils/builder" - . "github.com/kyma-project/lifecycle-manager/pkg/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + . "github.com/kyma-project/lifecycle-manager/pkg/testutils" ) const ( @@ -332,12 +333,11 @@ var _ = Describe("Test Reconciliation Skip label for Manifest", Ordered, func() var _ = Describe("Modules can only be referenced via module name", Ordered, func() { kyma := NewTestKyma("random-kyma") - moduleReferencedWithLabel := NewTestModuleWithFixName("random-module", v1beta2.DefaultChannel) - moduleReferencedWithNamespacedName := NewTestModuleWithFixName( + moduleReferencedWithLabel := NewTestModule("random-module", v1beta2.DefaultChannel) + moduleReferencedWithNamespacedName := NewTestModule( v1beta2.DefaultChannel+shared.Separator+"random-module", v1beta2.DefaultChannel) moduleReferencedWithFQDN := NewTestModuleWithFixName("kyma-project.io/module/"+"random-module", v1beta2.DefaultChannel) - kyma.Spec.Modules = append(kyma.Spec.Modules, moduleReferencedWithLabel) RegisterDefaultLifecycleForKyma(kyma) diff --git a/tests/integration/controller/kyma/moduletemplate_install_test.go b/tests/integration/controller/kyma/moduletemplate_install_test.go index b0e2d01098..ed9c1e656a 100644 --- a/tests/integration/controller/kyma/moduletemplate_install_test.go +++ b/tests/integration/controller/kyma/moduletemplate_install_test.go @@ -97,7 +97,7 @@ func givenKymaAndModuleTemplateCondition( } for _, module := range kyma.Spec.Modules { mtBuilder := builder.NewModuleTemplateBuilder(). - WithModuleName(module.Name). + WithLabelModuleName(module.Name). WithChannel(module.Channel). WithOCM(compdescv2.SchemaVersion) if isModuleTemplateInternal { diff --git a/tests/integration/controller/kyma/moduletemplate_test.go b/tests/integration/controller/kyma/moduletemplate_test.go index 115a8575db..7401db57a6 100644 --- a/tests/integration/controller/kyma/moduletemplate_test.go +++ b/tests/integration/controller/kyma/moduletemplate_test.go @@ -123,7 +123,7 @@ var _ = Describe("ModuleTemplate.Spec.descriptor contains RegistryCred label", O It("expect Manifest.Spec.installs contains credSecretSelector", func() { template := builder.NewModuleTemplateBuilder(). - WithModuleName(module.Name). + WithLabelModuleName(module.Name). WithChannel(module.Channel). WithOCMPrivateRepo().Build() Eventually(kcpClient.Create, Timeout, Interval).WithContext(ctx). diff --git a/tests/integration/controller/mandatorymodule/deletion/controller_test.go b/tests/integration/controller/mandatorymodule/deletion/controller_test.go index 2c97654043..6cd52188da 100644 --- a/tests/integration/controller/mandatorymodule/deletion/controller_test.go +++ b/tests/integration/controller/mandatorymodule/deletion/controller_test.go @@ -80,7 +80,7 @@ var _ = Describe("Mandatory Module Deletion", Ordered, func() { func registerControlPlaneLifecycleForKyma(kyma *v1beta2.Kyma) { template := builder.NewModuleTemplateBuilder(). WithName("mandatory-module"). - WithModuleName("mandatory-module"). + WithLabelModuleName("mandatory-module"). WithChannel(mandatoryChannel). WithMandatory(true). WithOCM(compdescv2.SchemaVersion).Build() diff --git a/tests/integration/controller/mandatorymodule/installation/controller_test.go b/tests/integration/controller/mandatorymodule/installation/controller_test.go index 2a8343271c..a808ccf1fc 100644 --- a/tests/integration/controller/mandatorymodule/installation/controller_test.go +++ b/tests/integration/controller/mandatorymodule/installation/controller_test.go @@ -78,7 +78,7 @@ var _ = Describe("Skipping Mandatory Module Installation", Ordered, func() { func registerControlPlaneLifecycleForKyma(kyma *v1beta2.Kyma) { template := builder.NewModuleTemplateBuilder(). - WithModuleName("mandatory-module"). + WithLabelModuleName("mandatory-module"). WithChannel(mandatoryChannel). WithMandatory(true). WithOCM(compdescv2.SchemaVersion).Build() diff --git a/tests/integration/controller/moduletemplate/moduletemplate_test.go b/tests/integration/controller/moduletemplate/moduletemplate_test.go new file mode 100644 index 0000000000..365ec5d414 --- /dev/null +++ b/tests/integration/controller/moduletemplate/moduletemplate_test.go @@ -0,0 +1,175 @@ +package moduletemplate_test + +import ( + "fmt" + + compdescv2 "github.com/open-component-model/ocm/pkg/contexts/ocm/compdesc/versions/v2" + + "github.com/kyma-project/lifecycle-manager/api/v1beta2" + "github.com/kyma-project/lifecycle-manager/pkg/testutils/builder" + + . "github.com/kyma-project/lifecycle-manager/pkg/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +const ( + thenNoErrors = " then expect no validation errors" + thenExpectAValidationError = " then expect a validation error" +) + +var _ = Describe("ModuleTemplate version is not empty", Ordered, func() { + module := NewTestModule("invalid-module", v1beta2.DefaultChannel) + + DescribeTable("Validate version", + func( + givenVersion string, + shouldSucceed bool, + ) { + template := builder.NewModuleTemplateBuilder(). + WithName(module.Name). + WithVersion(givenVersion). + WithModuleName(""). + WithChannel(module.Channel). + WithOCM(compdescv2.SchemaVersion).Build() + + err := kcpClient.Create(ctx, template) + if shouldSucceed { + Expect(err).NotTo(HaveOccurred()) + err = kcpClient.Delete(ctx, template) + Expect(err).NotTo(HaveOccurred()) + } else { + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("spec.version: Invalid value: \"%s\"", givenVersion))) + } + }, + Entry("when version is empty,"+ + thenNoErrors, + "", + true, + ), + Entry("when version contains one number,"+ + thenExpectAValidationError, + "1", + false, + ), + Entry("when version contains two numbers,"+ + thenExpectAValidationError, + "1.2", + false, + ), + Entry("when version contains three numbers,"+ + thenNoErrors, + "1.2.3", + true, + ), + Entry("when version is a word,"+ + thenExpectAValidationError, + "foo", + false, + ), + Entry("when version contains word and then semver,"+ + thenExpectAValidationError, + "foo-1.2.3", + false, + ), + Entry("when version contains semver and then a word,"+ + thenNoErrors, + "1.2.3-foo", + true, + ), + Entry("when version contains semver and two words,"+ + thenNoErrors, + "2.4.2-e2e-test", + true, + ), + Entry("when version contains semver and three words,"+ + thenNoErrors, + "2.4.2-e2e-test-foo", + true, + ), + Entry("when version contains lots of dashes"+ + thenNoErrors, + "1.2.3-----foo--bar-----baz", + true, + ), + Entry("when version contains semver with leading zero in the major"+ + thenExpectAValidationError, + "01.1.1", + false, + ), + Entry("when version contains semver with leading zero in the minor"+ + thenExpectAValidationError, + "0.01.1", + false, + ), + Entry("when version contains semver with leading zero in the patch"+ + thenExpectAValidationError, + "0.0.01", + false, + ), + Entry("when version contains semver with trailing letters in the patch"+ + thenExpectAValidationError, + "0.0.1asdf", + false, + ), + ) + DescribeTable("Validate moduleName", + func( + givenModuleName string, + shouldSucceed bool, + ) { + template := builder.NewModuleTemplateBuilder(). + WithName(module.Name). + WithModuleName(givenModuleName). + WithVersion(""). + WithChannel(module.Channel). + WithOCM(compdescv2.SchemaVersion).Build() + + err := kcpClient.Create(ctx, template) + if shouldSucceed { + Expect(err).NotTo(HaveOccurred()) + err = kcpClient.Delete(ctx, template) + Expect(err).NotTo(HaveOccurred()) + } else { + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("spec.moduleName: Invalid value: \"%s\"", givenModuleName))) + } + }, + Entry("when moduleName is empty,"+ + thenNoErrors, + "", + true, + ), + Entry("when moduleName is a single letter,"+ + thenExpectAValidationError, + "a", + false, + ), + Entry("when moduleName is a two-letter word,"+ + thenExpectAValidationError, + "ab", + false, + ), + Entry("when moduleName is a three-letter word,"+ + thenNoErrors, + "abc", + true, + ), + Entry("when moduleName contains a two-letter word,"+ + thenExpectAValidationError, + "abc-def-gh", + false, + ), + Entry("when moduleName contains two words,"+ + thenNoErrors, + "template-operator", + true, + ), + Entry("when moduleName contains a number,"+ + thenExpectAValidationError, + "template-operator23", + false, + ), + ) +}) diff --git a/tests/integration/controller/moduletemplate/suite_test.go b/tests/integration/controller/moduletemplate/suite_test.go new file mode 100644 index 0000000000..68f0db5ede --- /dev/null +++ b/tests/integration/controller/moduletemplate/suite_test.go @@ -0,0 +1,111 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package moduletemplate_test + +import ( + "context" + "path/filepath" + "testing" + + "go.uber.org/zap/zapcore" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + k8sclientscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/manager" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + + "github.com/kyma-project/lifecycle-manager/api" + "github.com/kyma-project/lifecycle-manager/internal" + "github.com/kyma-project/lifecycle-manager/internal/descriptor/provider" + "github.com/kyma-project/lifecycle-manager/pkg/log" + "github.com/kyma-project/lifecycle-manager/tests/integration" + + _ "github.com/open-component-model/ocm/pkg/contexts/ocm" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +const randomPort = "0" + +var ( + kcpClient client.Client + mgr manager.Manager + controlPlaneEnv *envtest.Environment + ctx context.Context + cancel context.CancelFunc + descriptorProvider *provider.CachedDescriptorProvider +) + +func TestAPIs(t *testing.T) { + t.Parallel() + RegisterFailHandler(Fail) + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + ctx, cancel = context.WithCancel(context.TODO()) + logf.SetLogger(log.ConfigLogger(9, zapcore.AddSync(GinkgoWriter))) + + By("bootstrapping test environment") + + controlPlaneEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join(integration.GetProjectRoot(), "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + } + + cfg, err := controlPlaneEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + Expect(api.AddToScheme(k8sclientscheme.Scheme)).NotTo(HaveOccurred()) + Expect(apiextensionsv1.AddToScheme(k8sclientscheme.Scheme)).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + mgr, err = ctrl.NewManager( + cfg, ctrl.Options{ + Metrics: metricsserver.Options{ + BindAddress: randomPort, + }, + Scheme: k8sclientscheme.Scheme, + Cache: internal.GetCacheOptions(false, "istio-system", "kcp-system", "kyma-system"), + }) + Expect(err).ToNot(HaveOccurred()) + + descriptorProvider = provider.NewCachedDescriptorProvider() + kcpClient = mgr.GetClient() + + go func() { + defer GinkgoRecover() + err = mgr.Start(ctx) + Expect(err).ToNot(HaveOccurred(), "failed to run manager") + }() +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + cancel() + + err := controlPlaneEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/tests/moduletemplates/moduletemplate_template_operator_v2_direct_version.yaml b/tests/moduletemplates/moduletemplate_template_operator_v2_direct_version.yaml new file mode 100755 index 0000000000..b2554672fa --- /dev/null +++ b/tests/moduletemplates/moduletemplate_template_operator_v2_direct_version.yaml @@ -0,0 +1,78 @@ +apiVersion: operator.kyma-project.io/v1beta2 +kind: ModuleTemplate +metadata: + name: template-operator-2.4.2-e2e-test + namespace: kcp-system + annotations: + "operator.kyma-project.io/doc-url": "https://kyma-project.io" + "operator.kyma-project.io/is-cluster-scoped": "false" +spec: + channel: none + mandatory: false + moduleName: template-operator + version: 2.4.2-e2e-test + data: + apiVersion: operator.kyma-project.io/v1alpha1 + kind: Sample + metadata: + name: sample-2.4.2-e2e-test + spec: + resourceFilePath: "./module-data/yaml" + descriptor: + component: + componentReferences: [ ] + labels: + - name: security.kyma-project.io/scan + value: enabled + version: v1 + name: kyma-project.io/module/template-operator + provider: '{"name":"kyma-project.io","labels":[{"name":"kyma-project.io/built-by","value":"cli","version":"v1"}]}' + repositoryContexts: + - baseUrl: europe-west3-docker.pkg.dev/sap-kyma-jellyfish-dev/template-operator + componentNameMapping: urlPath + type: OCIRegistry + resources: + - access: + globalAccess: + digest: sha256:b4e9c3717a84a3256b32d915752c0f034195e11d3a35fc468ed3036b6a91df5f + mediaType: application/octet-stream + ref: europe-west3-docker.pkg.dev/sap-kyma-jellyfish-dev/template-operator/component-descriptors/kyma-project.io/module/template-operator + size: 15217 + type: ociBlob + localReference: sha256:b4e9c3717a84a3256b32d915752c0f034195e11d3a35fc468ed3036b6a91df5f + mediaType: application/octet-stream + type: localBlob + name: raw-manifest + relation: local + type: yaml + version: 2.4.2-e2e-test + sources: + - access: + commit: 7935a702bf6b8173ada39564f8b874bb66b17ce0 + repoUrl: https://github.com/kyma-project/cli.git + type: gitHub + labels: + - name: git.kyma-project.io/ref + value: refs/heads/main + version: v1 + - name: scan.security.kyma-project.io/rc-tag + value: "" + version: v1 + - name: scan.security.kyma-project.io/language + value: golang-mod + version: v1 + - name: scan.security.kyma-project.io/dev-branch + value: "" + version: v1 + - name: scan.security.kyma-project.io/subprojects + value: "false" + version: v1 + - name: scan.security.kyma-project.io/exclude + value: '**/test/**,**/*_test.go,**/mocks/**' + version: v1 + name: module-sources + type: Github + version: 2.4.2-e2e-test + version: 2.4.2-e2e-test + meta: + schemaVersion: v2 diff --git a/unit-test-coverage.yaml b/unit-test-coverage.yaml index 434ef26794..e1fd5085fd 100644 --- a/unit-test-coverage.yaml +++ b/unit-test-coverage.yaml @@ -1,13 +1,13 @@ packages: - internal/crd: 92 - internal/descriptor/cache: 93 - internal/descriptor/provider: 66 + internal/crd: 92.1 + internal/descriptor/cache: 92.3 + internal/descriptor/provider: 66.7 internal/event: 100 internal/manifest/statecheck: 66 internal/manifest/filemutex: 100 internal/manifest: 7 - internal/istio: 63 - internal/pkg/resources: 91 - internal/remote: 6 - pkg/templatelookup: 69 + internal/istio: 63.5 + internal/pkg/resources: 91.8 + internal/remote: 6.6 + pkg/templatelookup: 72 diff --git a/verification.sh b/verification.sh new file mode 100755 index 0000000000..50f7565aeb --- /dev/null +++ b/verification.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash + +COMMIT_BASE="a180040ef2e6b8742a61ccc44eaaabae1dded8f5" + +FILE=".github/actions/deploy-template-operator/action.yaml" +FILE=".github/workflows/test-e2e.yaml" +FILE="api-version-compatibility-config.yaml" +FILE="api/shared/channel.go" +FILE="api/v1beta2/kyma_types.go" +FILE="api/v1beta2/moduletemplate_types.go" +FILE="api/v1beta2/moduletemplate_types_test.go" +FILE="api/v1beta2/zz_generated.deepcopy.go" +FILE="config/crd/bases/operator.kyma-project.io_moduletemplates.yaml" +FILE="internal/controller/kyma/controller.go" +FILE="internal/descriptor/cache/key.go" +FILE="internal/descriptor/provider/provider_test.go" +FILE="internal/pkg/metrics/kyma.go" +FILE="internal/remote/skr_context.go" +FILE="internal/remote/skr_context_test.go" +FILE="pkg/templatelookup/availableModules.go" +FILE="pkg/templatelookup/availableModules_test.go" +FILE="pkg/templatelookup/regular.go" +FILE="pkg/templatelookup/regular_test.go" +FILE="pkg/testutils/builder/moduletemplate.go" +FILE="pkg/testutils/moduletemplate.go" +FILE="pkg/testutils/utils.go" +FILE="tests/e2e/Makefile" +FILE="tests/e2e/module_install_by_version_test.go" +FILE="tests/e2e/module_status_decoupling_test.go" +FILE="tests/integration/apiwebhook/moduletemplate_crd_validation_test.go" +FILE="tests/integration/apiwebhook/ocm_test.go" +FILE="tests/integration/controller/kcp/helper_test.go" +FILE="tests/integration/controller/kcp/remote_sync_test.go" +FILE="tests/integration/controller/kyma/helper_test.go" +FILE="tests/integration/controller/kyma/kyma_module_channel_test.go" +FILE="tests/integration/controller/kyma/kyma_module_enable_test.go" +FILE="tests/integration/controller/kyma/kyma_module_version_test.go" +FILE="tests/integration/controller/kyma/kyma_test.go" +#TODO!FILE="tests/integration/controller/kyma/manifest_test.go" +FILE="tests/integration/controller/kyma/moduletemplate_install_test.go" +FILE="tests/integration/controller/kyma/moduletemplate_test.go" +FILE="tests/integration/controller/mandatorymodule/deletion/controller_test.go" +FILE="tests/integration/controller/mandatorymodule/installation/controller_test.go" +FILE="tests/integration/controller/moduletemplate/moduletemplate_test.go" +FILE="tests/integration/controller/moduletemplate/suite_test.go" +FILE="tests/moduletemplates/moduletemplate_template_operator_v2_direct_version.yaml" +#TODO!FILE="unit-test-coverage.yaml" + +#2nd round: +FILE="api/v1beta2/kyma_types.go" +FILE="internal/controller/kyma/controller.go" +#FILE="pkg/module/parse/template_to_module.go" +FILE="pkg/templatelookup/regular.go" +FILE="tests/integration/controller/kyma/manifest_test.go" +FILE="unit-test-coverage.yaml" + +git diff $COMMIT_BASE feat/module-catalogue-improvements $FILE > old.diff +git diff main $FILE > new.diff + +#NOTES +# internal/remote/skr_context_test.go: in commit 98811dee24447a000f82a0e1ed401db62e004b8d the "testCase" variable is modified -> possible bug +# check: "pkg/module/parse/template_to_module.go" for changes. +# +# +#