diff --git a/.github/actions/deploy-lifecycle-manager-e2e/action.yml b/.github/actions/deploy-lifecycle-manager-e2e/action.yml index b3fa733652..7420502417 100644 --- a/.github/actions/deploy-lifecycle-manager-e2e/action.yml +++ b/.github/actions/deploy-lifecycle-manager-e2e/action.yml @@ -21,6 +21,45 @@ runs: else echo "E2E_KUSTOMIZE_DIR=config/watcher_local_test" >> $GITHUB_ENV fi + - name: Patch local OCI registry host + if: ${{ matrix.e2e-test != 'oci-reg-cred-secret' && matrix.e2e-test != 'module-transferred-to-another-oci-registry' }} + working-directory: lifecycle-manager + shell: bash + run: | + pushd ${E2E_KUSTOMIZE_DIR} + echo \ + "- op: add + path: /spec/template/spec/containers/0/args/- + value: --oci-registry-host=http://k3d-kcp-registry.localhost:5000" >> oci_registry_host.yaml + cat oci_registry_host.yaml + kustomize edit add patch --path oci_registry_host.yaml --kind Deployment + popd + - name: Patch remote OCI registry host + if: ${{ matrix.e2e-test == 'module-transferred-to-another-oci-registry' }} + working-directory: lifecycle-manager + shell: bash + run: | + pushd ${E2E_KUSTOMIZE_DIR} + echo \ + "- op: add + path: /spec/template/spec/containers/0/args/- + value: --oci-registry-host=https://europe-west3-docker.pkg.dev/sap-kyma-jellyfish-dev/restricted-market" >> oci_registry_host.yaml + cat oci_registry_host.yaml + kustomize edit add patch --path oci_registry_host.yaml --kind Deployment + popd + - name: Patch private OCI registry secret + if: ${{ matrix.e2e-test == 'oci-reg-cred-secret' }} + working-directory: lifecycle-manager + shell: bash + run: | + pushd ${E2E_KUSTOMIZE_DIR} + echo \ + "- op: add + path: /spec/template/spec/containers/0/args/- + value: --oci-registry-cred-secret=private-oci-reg-creds" >> oci_registry_host.yaml + cat oci_registry_host.yaml + kustomize edit add patch --path oci_registry_host.yaml --kind Deployment + popd - name: Patch purge finalizer flags if: ${{ matrix.e2e-test == 'purge-controller' || matrix.e2e-test == 'purge-metrics'}} working-directory: lifecycle-manager @@ -148,14 +187,6 @@ runs: cat legacy-secret-rotation.yaml kustomize edit add patch --path legacy-secret-rotation.yaml --kind Deployment popd - - name: Use private OCI registry credentials - if: ${{matrix.e2e-test == 'oci-reg-cred-secret'}} - working-directory: lifecycle-manager - shell: bash - run: | - pushd ${E2E_KUSTOMIZE_DIR} - sed -i 's|value: --oci-registry-host=europe-docker.pkg.dev/kyma-project/kyma-modules|value: --oci-registry-cred-secret=private-oci-reg-creds|' kustomization.yaml - popd - name: Create and use maintenance window policy if: ${{matrix.e2e-test == 'maintenance-windows' || matrix.e2e-test == 'maintenance-windows-initial-installation' || diff --git a/.github/actions/deploy-template-operator-with-modulereleasemeta/action.yml b/.github/actions/deploy-template-operator-with-modulereleasemeta/action.yml index ea46a9a31f..ef46c1baef 100644 --- a/.github/actions/deploy-template-operator-with-modulereleasemeta/action.yml +++ b/.github/actions/deploy-template-operator-with-modulereleasemeta/action.yml @@ -79,10 +79,9 @@ runs: sed -i 's/k3d-private-oci-reg.localhost:5001/private-oci-reg.localhost:5000/g' ./template.yaml kubectl get crds kubectl apply -f template.yaml - - name: Create and apply ModuleReleaseMeta from the latest release + - name: Create and apply ModuleReleaseMeta from the template-operator repo working-directory: template-operator if: ${{ matrix.e2e-test == 'kyma-metrics' || - matrix.e2e-test == 'non-blocking-deletion' || matrix.e2e-test == 'purge-controller' || matrix.e2e-test == 'purge-metrics' || matrix.e2e-test == 'kyma-deprovision-with-foreground-propagation' || diff --git a/.github/scripts/debug/teardown.sh b/.github/scripts/debug/teardown.sh index 11da405a49..8a49878214 100755 --- a/.github/scripts/debug/teardown.sh +++ b/.github/scripts/debug/teardown.sh @@ -1,6 +1,23 @@ #!/usr/bin/env bash +kubectl config use-context k3d-kcp k3d cluster list +echo "--- KCP ModuleTemplate ---" +kubectl get moduletemplate -n kcp-system -o wide +kubectl get moduletemplate -n kcp-system -o yaml + +echo "--- KCP ModuleReleaseMeta ---" +kubectl get modulereleasemeta -n kcp-system -o wide +kubectl get modulereleasemeta -n kcp-system -o yaml + +echo "--- KCP Kyma ---" +kubectl get kyma -n kcp-system -o wide +kubectl get kyma -n kcp-system -o yaml + +echo "--- KCP Manifest ---" +kubectl get manifest -n kcp-system -o wide +kubectl get manifest -n kcp-system -o yaml + echo "--- KLM DEPLOYMENT ---" kubectl get deploy klm-controller-manager -n kcp-system -o yaml kubectl describe deploy klm-controller-manager -n kcp-system @@ -13,7 +30,14 @@ set -e kubectl config use-context k3d-skr + +echo "--- SKR DEPLOYMENT OVERVIEW ---" +kubectl get deploy -A -o wide + echo "--- SKR-WEBHOOK POD ---" +kubectl describe deploy/skr-webhook -n kyma-system kubectl get pods -l app=skr-webhook -n kyma-system -o wide + echo "--- SKR-WEBHOOK LOGS ---" kubectl logs deploy/skr-webhook -n kyma-system --container server + diff --git a/cmd/main.go b/cmd/main.go index 30b02f6317..c6a96e2499 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -24,6 +24,7 @@ import ( "net/http" "net/http/pprof" "os" + "strings" "time" certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" @@ -74,8 +75,10 @@ import ( "github.com/kyma-project/lifecycle-manager/internal/remote" "github.com/kyma-project/lifecycle-manager/internal/repository/istiogateway" kymarepository "github.com/kyma-project/lifecycle-manager/internal/repository/kyma" + "github.com/kyma-project/lifecycle-manager/internal/repository/oci" secretrepository "github.com/kyma-project/lifecycle-manager/internal/repository/secret" "github.com/kyma-project/lifecycle-manager/internal/service/accessmanager" + "github.com/kyma-project/lifecycle-manager/internal/service/componentdescriptor" "github.com/kyma-project/lifecycle-manager/internal/service/kyma/status/modules" "github.com/kyma-project/lifecycle-manager/internal/service/kyma/status/modules/generator" "github.com/kyma-project/lifecycle-manager/internal/service/kyma/status/modules/generator/fromerror" @@ -221,21 +224,45 @@ func setupManager(flagVar *flags.FlagVar, cacheOptions cache.Options, scheme *ma } sharedMetrics := metrics.NewSharedMetrics() - descriptorProvider := provider.NewCachedDescriptorProvider() + + ociRegistryHost := getOciRegistryHost(mgr.GetConfig(), flagVar, logger) + var insecure bool + + if noSchemeRef, found := strings.CutPrefix(ociRegistryHost, "http://"); found { + insecure = true + ociRegistryHost = noSchemeRef + } else if noSchemeRef, found := strings.CutPrefix(ociRegistryHost, "https://"); found { + ociRegistryHost = noSchemeRef + } + + ocmDescriptorRepository, err := oci.NewRepository( + keychainLookupFromFlag(mgr.GetClient(), flagVar), + ociRegistryHost, + insecure, + ) + if err != nil { + logger.Error(err, "failed to create OCM descriptor repository") + os.Exit(bootstrapFailedExitCode) + } + + ocmDescriptorService, err := componentdescriptor.NewService(ocmDescriptorRepository) + if err != nil { + logger.Error(err, "failed to create OCM descriptor service") + os.Exit(bootstrapFailedExitCode) + } + descriptorProvider := provider.NewCachedDescriptorProvider(ocmDescriptorService) + kymaMetrics := metrics.NewKymaMetrics(sharedMetrics) mandatoryModulesMetrics := metrics.NewMandatoryModulesMetrics() maintenanceWindow := initMaintenanceWindow(flagVar.MinMaintenanceWindowSize, logger) metrics.NewFipsMetrics().Update() - //nolint:godox // this will be used in the future - // TODO: use the oci registry host //nolint:godox // this will be used in the future - _ = getOciRegistryHost(mgr.GetConfig(), flagVar, logger) - - setupKymaReconciler(mgr, descriptorProvider, skrContextProvider, eventRecorder, flagVar, options, skrWebhookManager, - kymaMetrics, logger, maintenanceWindow) - setupManifestReconciler(mgr, flagVar, options, sharedMetrics, mandatoryModulesMetrics, accessManagerService, logger, - eventRecorder) - setupMandatoryModuleReconciler(mgr, descriptorProvider, flagVar, options, mandatoryModulesMetrics, logger) + setupKymaReconciler(mgr, descriptorProvider, skrContextProvider, eventRecorder, + flagVar, options, skrWebhookManager, kymaMetrics, logger, maintenanceWindow, ociRegistryHost) + setupManifestReconciler(mgr, flagVar, options, sharedMetrics, mandatoryModulesMetrics, + accessManagerService, logger, eventRecorder) + setupMandatoryModuleReconciler(mgr, descriptorProvider, flagVar, options, + mandatoryModulesMetrics, logger, ociRegistryHost) setupMandatoryModuleDeletionReconciler(mgr, descriptorProvider, eventRecorder, flagVar, options, logger) if flagVar.EnablePurgeFinalizer { setupPurgeReconciler(mgr, skrContextProvider, eventRecorder, flagVar, options, logger) @@ -379,7 +406,7 @@ func scheduleMetricsCleanup(kymaMetrics *metrics.KymaMetrics, cleanupIntervalInM func setupKymaReconciler(mgr ctrl.Manager, descriptorProvider *provider.CachedDescriptorProvider, skrContextFactory remote.SkrContextProvider, event event.Event, flagVar *flags.FlagVar, options ctrlruntime.Options, skrWebhookManager *watcher.SkrWebhookManifestManager, kymaMetrics *metrics.KymaMetrics, - setupLog logr.Logger, maintenanceWindow maintenancewindows.MaintenanceWindow, + setupLog logr.Logger, maintenanceWindow maintenancewindows.MaintenanceWindow, ociRegistryHost string, ) { options.RateLimiter = internal.RateLimiter(flagVar.FailureBaseDelay, flagVar.FailureMaxDelay, flagVar.RateLimiterFrequency, flagVar.RateLimiterBurst) @@ -420,6 +447,7 @@ func setupKymaReconciler(mgr ctrl.Manager, descriptorProvider *provider.CachedDe flagVar.RemoteSyncNamespace), TemplateLookup: templatelookup.NewTemplateLookup(kcpClient, descriptorProvider, moduleTemplateInfoLookupStrategies), + OCIRegistryHost: ociRegistryHost, }).SetupWithManager( mgr, options, kyma.SetupOptions{ ListenerAddr: flagVar.KymaListenerAddr, @@ -476,7 +504,7 @@ func setupManifestReconciler(mgr ctrl.Manager, manifestClient := manifestclient.NewManifestClient(event, mgr.GetClient()) orphanDetectionClient := kymarepository.NewClient(mgr.GetClient()) orphanDetectionService := orphan.NewDetectionService(orphanDetectionClient) - specResolver := spec.NewResolver(keychainLookupFromFlag(mgr, flagVar), img.NewPathExtractor()) + specResolver := spec.NewResolver(keychainLookupFromFlag(mgr.GetClient(), flagVar), img.NewPathExtractor()) clientCache := skrclientcache.NewService() skrClient := skrclient.NewService(mgr.GetConfig().QPS, mgr.GetConfig().Burst, accessManagerService) @@ -504,9 +532,9 @@ func setupManifestReconciler(mgr ctrl.Manager, } //nolint:ireturn // constructor functions can return interfaces -func keychainLookupFromFlag(mgr ctrl.Manager, flagVar *flags.FlagVar) spec.KeyChainLookup { +func keychainLookupFromFlag(clnt client.Client, flagVar *flags.FlagVar) spec.KeyChainLookup { if flagVar.OciRegistryCredSecretName != "" { - return keychainprovider.NewFromSecretKeyChainProvider(mgr.GetClient(), + return keychainprovider.NewFromSecretKeyChainProvider(clnt, types.NamespacedName{ Namespace: shared.DefaultControlPlaneNamespace, Name: flagVar.OciRegistryCredSecretName, @@ -547,6 +575,7 @@ func setupMandatoryModuleReconciler(mgr ctrl.Manager, options ctrlruntime.Options, metrics *metrics.MandatoryModulesMetrics, setupLog logr.Logger, + ociRegistryHost string, ) { options.RateLimiter = internal.RateLimiter(flagVar.FailureBaseDelay, flagVar.FailureMaxDelay, flagVar.RateLimiterFrequency, flagVar.RateLimiterBurst) @@ -564,6 +593,7 @@ func setupMandatoryModuleReconciler(mgr ctrl.Manager, RemoteSyncNamespace: flagVar.RemoteSyncNamespace, DescriptorProvider: descriptorProvider, Metrics: metrics, + OCIRegistryHost: ociRegistryHost, }).SetupWithManager(mgr, options); err != nil { setupLog.Error(err, "unable to create controller", "controller", "MandatoryModule") os.Exit(bootstrapFailedExitCode) diff --git a/config/watcher_local_test/kustomization.yaml b/config/watcher_local_test/kustomization.yaml index c2eb6084b7..4f395d07de 100644 --- a/config/watcher_local_test/kustomization.yaml +++ b/config/watcher_local_test/kustomization.yaml @@ -81,9 +81,6 @@ patches: - op: add path: /spec/template/spec/containers/0/args/- value: --leader-election-retry-period=3s - - op: add - path: /spec/template/spec/containers/0/args/- - value: --oci-registry-host=europe-docker.pkg.dev/kyma-project/kyma-modules - op: replace path: /spec/template/spec/containers/0/imagePullPolicy value: Always diff --git a/config/watcher_local_test_gcm/kustomization.yaml b/config/watcher_local_test_gcm/kustomization.yaml index c05db7e781..4ed08413db 100644 --- a/config/watcher_local_test_gcm/kustomization.yaml +++ b/config/watcher_local_test_gcm/kustomization.yaml @@ -77,9 +77,6 @@ patches: - op: add path: /spec/template/spec/containers/0/args/- value: --leader-election-retry-period=3s - - op: add - path: /spec/template/spec/containers/0/args/- - value: --oci-registry-host=europe-docker.pkg.dev/kyma-project/kyma-modules - op: replace path: /spec/template/spec/containers/0/imagePullPolicy value: Always diff --git a/internal/controller/kyma/controller.go b/internal/controller/kyma/controller.go index 243f4aa4e7..9fc9d1398f 100644 --- a/internal/controller/kyma/controller.go +++ b/internal/controller/kyma/controller.go @@ -86,6 +86,7 @@ type Reconciler struct { Metrics *metrics.KymaMetrics RemoteCatalog *remote.RemoteCatalog TemplateLookup *templatelookup.TemplateLookup + OCIRegistryHost string } func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { @@ -584,7 +585,7 @@ func (r *Reconciler) updateKyma(ctx context.Context, kyma *v1beta2.Kyma) error { func (r *Reconciler) reconcileManifests(ctx context.Context, kyma *v1beta2.Kyma) error { templates := r.TemplateLookup.GetRegularTemplates(ctx, kyma) - prsr := parser.NewParser(r.Client, r.DescriptorProvider, r.RemoteSyncNamespace) + prsr := parser.NewParser(r.Client, r.DescriptorProvider, r.RemoteSyncNamespace, r.OCIRegistryHost) modules := prsr.GenerateModulesFromTemplates(kyma, templates) runner := sync.New(r) diff --git a/internal/controller/mandatorymodule/deletion_controller.go b/internal/controller/mandatorymodule/deletion_controller.go index f2cf0df3b7..50c8dae30e 100644 --- a/internal/controller/mandatorymodule/deletion_controller.go +++ b/internal/controller/mandatorymodule/deletion_controller.go @@ -29,9 +29,11 @@ import ( "github.com/kyma-project/lifecycle-manager/api/shared" "github.com/kyma-project/lifecycle-manager/api/v1beta2" "github.com/kyma-project/lifecycle-manager/internal/descriptor/provider" + "github.com/kyma-project/lifecycle-manager/internal/descriptor/types/ocmidentity" "github.com/kyma-project/lifecycle-manager/internal/event" "github.com/kyma-project/lifecycle-manager/pkg/log" "github.com/kyma-project/lifecycle-manager/pkg/queue" + "github.com/kyma-project/lifecycle-manager/pkg/templatelookup" "github.com/kyma-project/lifecycle-manager/pkg/util" ) @@ -76,7 +78,18 @@ func (r *DeletionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c return ctrl.Result{}, nil } - manifests, err := r.getCorrespondingManifests(ctx, template) + mrm, err := r.GetModuleReleaseMeta(ctx, template.Spec.ModuleName, template.Namespace) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to find ModuleReleaseMeta for Mandatory Module %s: %w", + template.Name, err) + } + ocmi, err := ocmidentity.NewComponentId(mrm.Spec.OcmComponentName, template.Spec.Version) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to create OCM identity for Mandatory Module %s: %w", + template.Spec.ModuleName, err) + } + + manifests, err := r.getCorrespondingManifests(ctx, template.Namespace, *ocmi) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to get MandatoryModuleManifests: %w", err) } @@ -96,6 +109,12 @@ func (r *DeletionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c return ctrl.Result{Requeue: true}, nil } +func (r *DeletionReconciler) GetModuleReleaseMeta(ctx context.Context, moduleName, namespace string) ( + *v1beta2.ModuleReleaseMeta, error, +) { + return templatelookup.GetModuleReleaseMeta(ctx, r.Client, moduleName, namespace) +} + func (r *DeletionReconciler) updateTemplateFinalizer(ctx context.Context, template *v1beta2.ModuleTemplate, ) (ctrl.Result, error) { @@ -107,22 +126,18 @@ func (r *DeletionReconciler) updateTemplateFinalizer(ctx context.Context, } func (r *DeletionReconciler) getCorrespondingManifests(ctx context.Context, - template *v1beta2.ModuleTemplate) ([]v1beta2.Manifest, + namespace string, ocmi ocmidentity.ComponentId) ([]v1beta2.Manifest, error, ) { manifests := &v1beta2.ManifestList{} - descriptor, err := r.DescriptorProvider.GetDescriptor(template) - if err != nil { - return nil, fmt.Errorf("not able to get descriptor from template: %w", err) - } if err := r.List(ctx, manifests, &client.ListOptions{ - Namespace: template.Namespace, + Namespace: namespace, LabelSelector: k8slabels.SelectorFromSet(k8slabels.Set{shared.IsMandatoryModule: "true"}), }); client.IgnoreNotFound(err) != nil { return nil, fmt.Errorf("not able to list mandatory module manifests: %w", err) } - filtered := filterManifestsByFQDNAndVersion(manifests.Items, descriptor.GetName(), descriptor.GetVersion()) + filtered := filterManifestsByComponentIdentity(manifests.Items, ocmi) return filtered, nil } @@ -137,8 +152,10 @@ func (r *DeletionReconciler) removeManifests(ctx context.Context, manifests []v1 return nil } -func filterManifestsByFQDNAndVersion(manifests []v1beta2.Manifest, - fqdn, moduleVersion string, +// filterManifestsByComponentIdentity filters the manifests by OCM Component Name and module version. +// OCM Component Name is a fully qualified name that looks like: 'kyma-project.io/module/'. +func filterManifestsByComponentIdentity(manifests []v1beta2.Manifest, + ocmi ocmidentity.ComponentId, ) []v1beta2.Manifest { filteredManifests := make([]v1beta2.Manifest, 0) for _, manifest := range manifests { @@ -146,7 +163,7 @@ func filterManifestsByFQDNAndVersion(manifests []v1beta2.Manifest, continue } - if manifest.Annotations[shared.FQDN] == fqdn && manifest.Spec.Version == moduleVersion { + if manifest.Annotations[shared.FQDN] == ocmi.Name() && manifest.Spec.Version == ocmi.Version() { filteredManifests = append(filteredManifests, manifest) } } diff --git a/internal/controller/mandatorymodule/installation_controller.go b/internal/controller/mandatorymodule/installation_controller.go index 82b9ec42ac..c06b5e134c 100644 --- a/internal/controller/mandatorymodule/installation_controller.go +++ b/internal/controller/mandatorymodule/installation_controller.go @@ -18,6 +18,7 @@ package mandatorymodule import ( "context" + "errors" "fmt" ctrl "sigs.k8s.io/controller-runtime" @@ -26,6 +27,7 @@ import ( "github.com/kyma-project/lifecycle-manager/api/v1beta2" "github.com/kyma-project/lifecycle-manager/internal/descriptor/provider" + "github.com/kyma-project/lifecycle-manager/internal/descriptor/types/ocmidentity" "github.com/kyma-project/lifecycle-manager/internal/manifest/parser" "github.com/kyma-project/lifecycle-manager/internal/pkg/metrics" "github.com/kyma-project/lifecycle-manager/pkg/log" @@ -43,8 +45,11 @@ type InstallationReconciler struct { DescriptorProvider *provider.CachedDescriptorProvider RemoteSyncNamespace string Metrics *metrics.MandatoryModulesMetrics + OCIRegistryHost string } +var ErrNoModuleReleaseMeta = errors.New("no ModuleReleaseMeta found") + func (r *InstallationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := logf.FromContext(ctx) logger.V(log.DebugLevel).Info("Mandatory Module Reconciliation started") @@ -68,13 +73,22 @@ func (r *InstallationReconciler) Reconcile(ctx context.Context, req ctrl.Request if err != nil { return emptyResultWithErr(err) } + + // Note: Here we're just adding OCM identity information. + // It doesn't change how the Mandatory Modules are selected for installation: + // we still take the latest version of every ModuleTemplate which is marked as mandatory. + // The switch to the logic based on ModuleReleaseMeta will be done in a follow-up PR. + // However, the first step towards this switch is already done here: + // the OCM identity information is taken from the ModuleReleaseMeta instance, + // that should exist in the cluster. + r.extendWithOCMIdentities(ctx, mandatoryTemplates) + r.Metrics.RecordMandatoryTemplatesCount(len(mandatoryTemplates)) modules, err := r.GenerateModulesFromTemplate(ctx, mandatoryTemplates, kyma) if err != nil { return emptyResultWithErr(err) } - runner := sync.New(r) if err := runner.ReconcileManifests(ctx, kyma, modules); err != nil { return emptyResultWithErr(err) @@ -86,10 +100,55 @@ func (r *InstallationReconciler) Reconcile(ctx context.Context, req ctrl.Request func (r *InstallationReconciler) GenerateModulesFromTemplate(ctx context.Context, templates templatelookup.ModuleTemplatesByModuleName, kyma *v1beta2.Kyma, ) (modulecommon.Modules, error) { - parser := parser.NewParser(r.Client, r.DescriptorProvider, r.RemoteSyncNamespace) + parser := parser.NewParser(r.Client, r.DescriptorProvider, r.RemoteSyncNamespace, r.OCIRegistryHost) return parser.GenerateMandatoryModulesFromTemplates(ctx, kyma, templates), nil } +func (r *InstallationReconciler) GetModuleReleaseMeta(ctx context.Context, moduleName, namespace string) ( + *v1beta2.ModuleReleaseMeta, error, +) { + return templatelookup.GetModuleReleaseMeta(ctx, r.Client, moduleName, namespace) +} + +// extendWithOCMIdentities extends every ModuleTemplateInfo in the given map with OCM identities. +func (r *InstallationReconciler) extendWithOCMIdentities( + ctx context.Context, + templates templatelookup.ModuleTemplatesByModuleName, +) { + for _, template := range templates { + if template.Err != nil { + continue + } + + mrm, err := r.GetModuleReleaseMeta(ctx, template.Spec.ModuleName, template.Namespace) + if client.IgnoreNotFound(err) != nil { // errors other than NotFound + template.Err = fmt.Errorf("failed getting ModuleReleaseMeta for module %s in namespace %s: %w", + template.Spec.ModuleName, template.Namespace, err) + continue + } + + // Note: this MUST be treated as an error, as every mandatory ModuleTemplate + // must have a corresponding ModuleReleaseMeta. + // Otherwise the module can't be installed, because without a ModuleReleaseMeta there is + // no way to fetch the ComponentDescriptor for the Module. + if mrm == nil { + template.Err = fmt.Errorf("%w for mandatory module %s in namespace %s", + ErrNoModuleReleaseMeta, template.Spec.ModuleName, template.Namespace) + continue + } + + if template.ComponentId == nil { + ocmi, err := ocmidentity.NewComponentId(mrm.Spec.OcmComponentName, template.Spec.Version) + if err != nil { + template.Err = fmt.Errorf("failed creating OCM identity for module %s in namespace %s: %w", + template.Spec.ModuleName, template.Namespace, err) + continue + } + template.ComponentId = ocmi + } + } +} + func emptyResultWithErr(err error) (ctrl.Result, error) { return ctrl.Result{}, fmt.Errorf("MandatoryModuleController: %w", err) } diff --git a/internal/descriptor/cache/cache.go b/internal/descriptor/cache/cache.go index ae5b4b1ff7..60e6f00aea 100644 --- a/internal/descriptor/cache/cache.go +++ b/internal/descriptor/cache/cache.go @@ -1,9 +1,11 @@ package cache import ( + "fmt" "sync" "github.com/kyma-project/lifecycle-manager/internal/descriptor/types" + "github.com/kyma-project/lifecycle-manager/internal/descriptor/types/ocmidentity" ) type DescriptorCache struct { @@ -16,7 +18,7 @@ func NewDescriptorCache() *DescriptorCache { } } -func (d *DescriptorCache) Get(key string) *types.Descriptor { +func (d *DescriptorCache) Get(key DescriptorKey) *types.Descriptor { value, ok := d.cache.Load(key) if !ok { return nil @@ -29,6 +31,12 @@ func (d *DescriptorCache) Get(key string) *types.Descriptor { return &types.Descriptor{ComponentDescriptor: desc.Copy()} } -func (d *DescriptorCache) Set(key string, value *types.Descriptor) { +func (d *DescriptorCache) Set(key DescriptorKey, value *types.Descriptor) { d.cache.Store(key, value) } + +type DescriptorKey string + +func GenerateDescriptorKey(ocmi ocmidentity.ComponentId) DescriptorKey { + return DescriptorKey(fmt.Sprintf("%s:%s", ocmi.Name(), ocmi.Version())) +} diff --git a/internal/descriptor/cache/cache_test.go b/internal/descriptor/cache/cache_test.go index 0b0944ada5..cf403cb1a1 100644 --- a/internal/descriptor/cache/cache_test.go +++ b/internal/descriptor/cache/cache_test.go @@ -15,7 +15,7 @@ func TestGet_ForCacheWithoutEntry_ReturnsNoEntry(t *testing.T) { descriptorCache := cache.NewDescriptorCache() key := "key 1" - actual := descriptorCache.Get(key) + actual := descriptorCache.Get(cache.DescriptorKey(key)) assert.Nil(t, actual) } @@ -32,9 +32,9 @@ func TestGet_ForCacheWithAnEntry_ReturnsAnEntry(t *testing.T) { } desc1 := &types.Descriptor{ComponentDescriptor: ocmDesc1} - descriptorCache.Set(key1, desc1) + descriptorCache.Set(cache.DescriptorKey(key1), desc1) - assertDescriptorEqual(t, desc1, descriptorCache.Get(key1)) + assertDescriptorEqual(t, desc1, descriptorCache.Get(cache.DescriptorKey(key1))) } func TestGet_ForCacheWithOverwrittenEntry_ReturnsNewEntry(t *testing.T) { @@ -53,15 +53,15 @@ func TestGet_ForCacheWithOverwrittenEntry_ReturnsNewEntry(t *testing.T) { }, }, } - descriptorCache.Set(originalKey, originalValue) - assertDescriptorNotEqual(t, newValue, descriptorCache.Get(originalKey)) - assert.Nil(t, descriptorCache.Get(newKey)) + descriptorCache.Set(cache.DescriptorKey(originalKey), originalValue) + assertDescriptorNotEqual(t, newValue, descriptorCache.Get(cache.DescriptorKey(originalKey))) + assert.Nil(t, descriptorCache.Get(cache.DescriptorKey(newKey))) - descriptorCache.Set(newKey, newValue) - descriptorCache.Set(originalKey, newValue) + descriptorCache.Set(cache.DescriptorKey(newKey), newValue) + descriptorCache.Set(cache.DescriptorKey(originalKey), newValue) - assertDescriptorEqual(t, newValue, descriptorCache.Get(newKey)) - assertDescriptorEqual(t, newValue, descriptorCache.Get(originalKey)) + assertDescriptorEqual(t, newValue, descriptorCache.Get(cache.DescriptorKey(newKey))) + assertDescriptorEqual(t, newValue, descriptorCache.Get(cache.DescriptorKey(originalKey))) } func assertDescriptorEqual(t *testing.T, expected, actual *types.Descriptor) { diff --git a/internal/descriptor/cache/key_test.go b/internal/descriptor/cache/key_test.go new file mode 100644 index 0000000000..a3510d78f8 --- /dev/null +++ b/internal/descriptor/cache/key_test.go @@ -0,0 +1,34 @@ +package cache_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/kyma-project/lifecycle-manager/internal/descriptor/cache" + "github.com/kyma-project/lifecycle-manager/pkg/testutils" +) + +func TestGenerateDescriptorCacheKey(t *testing.T) { + testCases := []struct { + name string + moduleName string + moduleVersion string + want string + }{ + { + name: "with valid module name and version", + moduleName: "name", + moduleVersion: "1.0.0", + want: "name:1.0.0", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ocmi := testutils.MustNewComponentId(tc.moduleName, tc.moduleVersion) + got := cache.GenerateDescriptorKey(*ocmi) + assert.Equal(t, tc.want, string(got)) + }) + } +} diff --git a/internal/descriptor/provider/provider.go b/internal/descriptor/provider/provider.go index 01f067fda0..cd419b24ee 100644 --- a/internal/descriptor/provider/provider.go +++ b/internal/descriptor/provider/provider.go @@ -1,109 +1,101 @@ package provider import ( + "context" "errors" "fmt" - "ocm.software/ocm/api/ocm/compdesc" - - "github.com/kyma-project/lifecycle-manager/api/v1beta2" "github.com/kyma-project/lifecycle-manager/internal/descriptor/cache" "github.com/kyma-project/lifecycle-manager/internal/descriptor/types" + "github.com/kyma-project/lifecycle-manager/internal/descriptor/types/ocmidentity" ) var ( - ErrTypeAssert = errors.New("failed to convert to v1beta2.Descriptor") - ErrDecode = errors.New("failed to decode to descriptor target") - ErrTemplateNil = errors.New("module template is nil") - ErrDescriptorNil = errors.New("module template contains nil descriptor") + ErrDecode = errors.New("failed to decode to descriptor target") + ErrNilProvider = errors.New("OCMIProvider is nil") + ErrNilIdentity = errors.New("component identity is nil") + ErrNameOrVersionEmpty = errors.New("component name or version is empty") ) +type DescriptorService interface { + GetComponentDescriptor(ctx context.Context, ocmi ocmidentity.ComponentId) (*types.Descriptor, error) +} + type CachedDescriptorProvider struct { - DescriptorCache *cache.DescriptorCache + descriptorCache *cache.DescriptorCache + descriptorService DescriptorService } -func NewCachedDescriptorProvider() *CachedDescriptorProvider { +func NewCachedDescriptorProvider(service DescriptorService) *CachedDescriptorProvider { return &CachedDescriptorProvider{ - DescriptorCache: cache.NewDescriptorCache(), + descriptorCache: cache.NewDescriptorCache(), + descriptorService: service, } } -func (c *CachedDescriptorProvider) GetDescriptor(template *v1beta2.ModuleTemplate) (*types.Descriptor, error) { - if template == nil { - return nil, ErrTemplateNil - } - - if template.Spec.Descriptor.Object != nil { - desc, ok := template.Spec.Descriptor.Object.(*types.Descriptor) - if !ok { - return nil, ErrTypeAssert - } - - if desc == nil { - return nil, ErrDescriptorNil - } +// Convenience interface to get the OCM identity of a component from objects +// that already have all required data. +// Then we don't have to create intermediate variables of type ocmidentity.Component. +type OCMIProvider interface { + GetOCMIdentity() (*ocmidentity.ComponentId, error) +} - return desc, nil +func (c *CachedDescriptorProvider) Add(ocmi ocmidentity.ComponentId) error { + if ocmi.Name() == "" || ocmi.Version() == "" { + return fmt.Errorf("cannot get descriptor for component: %w", ErrNameOrVersionEmpty) } - key := c.GenerateDescriptorKey(template.Name, template.Spec.Version) - - descriptor := c.DescriptorCache.Get(key) + key := cache.GenerateDescriptorKey(ocmi) + descriptor := c.descriptorCache.Get(key) if descriptor != nil { - return descriptor, nil + return nil } - ocmDesc, err := compdesc.Decode( - template.Spec.Descriptor.Raw, []compdesc.DecodeOption{compdesc.DisableValidation(true)}..., - ) - if err != nil { - return nil, errors.Join(ErrDecode, err) - } + ctx, cancel := context.WithCancel(context.Background()) + descriptor, err := c.descriptorService.GetComponentDescriptor(ctx, ocmi) + defer cancel() - template.Spec.Descriptor.Object = &types.Descriptor{ComponentDescriptor: ocmDesc} - descriptor, ok := template.Spec.Descriptor.Object.(*types.Descriptor) - if !ok { - return nil, ErrTypeAssert + if err != nil { + return fmt.Errorf("error finding ComponentDescriptor: %w", err) } - return descriptor, nil -} + c.descriptorCache.Set(key, descriptor) -func (c *CachedDescriptorProvider) GenerateDescriptorKey(name, version string) string { - return fmt.Sprintf("%s:%s", name, version) + return nil } -func (c *CachedDescriptorProvider) Add(template *v1beta2.ModuleTemplate) error { - if template == nil { - return ErrTemplateNil +func (c *CachedDescriptorProvider) GetDescriptor(ocmi ocmidentity.ComponentId) (*types.Descriptor, error) { + if ocmi.Name() == "" || ocmi.Version() == "" { + return nil, fmt.Errorf("cannot get descriptor for component: %w", ErrNameOrVersionEmpty) } - - key := c.GenerateDescriptorKey(template.Name, template.Spec.Version) - descriptor := c.DescriptorCache.Get(key) + key := cache.GenerateDescriptorKey(ocmi) + descriptor := c.descriptorCache.Get(key) if descriptor != nil { - return nil + return descriptor, nil } - if template.Spec.Descriptor.Object != nil { - desc, ok := template.Spec.Descriptor.Object.(*types.Descriptor) - if ok && desc != nil { - c.DescriptorCache.Set(key, desc) - return nil - } - } + ctx, cancel := context.WithCancel(context.Background()) + descriptor, err := c.descriptorService.GetComponentDescriptor(ctx, ocmi) + defer cancel() - ocmDesc, err := compdesc.Decode( - template.Spec.Descriptor.Raw, []compdesc.DecodeOption{compdesc.DisableValidation(true)}..., - ) if err != nil { - return errors.Join(ErrDecode, err) + return nil, fmt.Errorf("error finding ComponentDescriptor: %w", err) + } + + return descriptor, nil +} + +func (c *CachedDescriptorProvider) GetDescriptorWithIdentity(ocp OCMIProvider) (*types.Descriptor, error) { + if ocp == nil { + return nil, fmt.Errorf("failed to get component identity from provider: %w", ErrNilProvider) } - template.Spec.Descriptor.Object = &types.Descriptor{ComponentDescriptor: ocmDesc} - descriptor, ok := template.Spec.Descriptor.Object.(*types.Descriptor) - if !ok { - return ErrTypeAssert + ocmi, err := ocp.GetOCMIdentity() + if err != nil { + return nil, fmt.Errorf("failed to get component identity from provider: %w", err) + } + if ocmi == nil { + return nil, fmt.Errorf("failed to get component identity from provider: %w", ErrNilIdentity) } - c.DescriptorCache.Set(key, descriptor) - return nil + return c.GetDescriptor(*ocmi) } diff --git a/internal/descriptor/provider/provider_test.go b/internal/descriptor/provider/provider_test.go index fd6665aa19..cd38041acb 100644 --- a/internal/descriptor/provider/provider_test.go +++ b/internal/descriptor/provider/provider_test.go @@ -1,139 +1,172 @@ package provider_test import ( + "errors" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "ocm.software/ocm/api/ocm/compdesc" "github.com/kyma-project/lifecycle-manager/api/v1beta2" - "github.com/kyma-project/lifecycle-manager/internal/descriptor/cache" "github.com/kyma-project/lifecycle-manager/internal/descriptor/provider" - "github.com/kyma-project/lifecycle-manager/internal/descriptor/types" + "github.com/kyma-project/lifecycle-manager/internal/descriptor/types/ocmidentity" + "github.com/kyma-project/lifecycle-manager/internal/service/componentdescriptor" "github.com/kyma-project/lifecycle-manager/pkg/testutils/builder" ) -func TestGetDescriptor_OnEmptySpec_ReturnsErrDecode(t *testing.T) { - descriptorProvider := provider.NewCachedDescriptorProvider() // assuming it handles nil cache internally - template := &v1beta2.ModuleTemplate{} - - _, err := descriptorProvider.GetDescriptor(template) +func TestGetDescriptor_OnEmptyIdentity_ReturnsErr(t *testing.T) { + descriptorProvider := provider.NewCachedDescriptorProvider(nil) + _, err := descriptorProvider.GetDescriptor(ocmidentity.ComponentId{}) require.Error(t, err) - require.ErrorIs(t, err, provider.ErrDecode) + require.ErrorIs(t, err, provider.ErrNameOrVersionEmpty) } -func TestAdd_OnNilTemplate_ReturnsErrTemplateNil(t *testing.T) { - descriptorProvider := provider.NewCachedDescriptorProvider() - - err := descriptorProvider.Add(nil) +func TestAdd_OnEmptyIdentity_ReturnsErr(t *testing.T) { + descriptorProvider := provider.NewCachedDescriptorProvider(nil) + err := descriptorProvider.Add(ocmidentity.ComponentId{}) require.Error(t, err) - require.ErrorIs(t, err, provider.ErrTemplateNil) + require.ErrorIs(t, err, provider.ErrNameOrVersionEmpty) } -func TestGetDescriptor_OnNilTemplate_ReturnsErrTemplateNil(t *testing.T) { - descriptorProvider := provider.NewCachedDescriptorProvider() - - _, err := descriptorProvider.GetDescriptor(nil) +func TestGetDescriptor_OnInvalidRawDescriptor_ReturnsErr(t *testing.T) { + descriptorProvider := provider.NewCachedDescriptorProvider( + (&componentdescriptor.FakeService{}).Register([]byte("invalid descriptor"))) + ocmi, err := ocmidentity.NewComponentId("test", "v1") + require.NoError(t, err) + _, err = descriptorProvider.GetDescriptor(*ocmi) require.Error(t, err) - require.ErrorIs(t, err, provider.ErrTemplateNil) + require.ErrorIs(t, err, componentdescriptor.ErrDecode) } -func TestGetDescriptor_OnInvalidRawDescriptor_ReturnsErrDescriptorNil(t *testing.T) { - descriptorProvider := provider.NewCachedDescriptorProvider() - template := builder.NewModuleTemplateBuilder(). - WithRawDescriptor([]byte("invalid descriptor")). - WithDescriptor(nil). - Build() +func TestGetDescriptor_OnEmptyCache_ReturnsDescriptorFromService(t *testing.T) { + // given + var moduleTemplateFromFile v1beta2.ModuleTemplate + builder.ReadComponentDescriptorFromFile("v1beta2_template_operator_new_ocm.yaml", &moduleTemplateFromFile) - _, err := descriptorProvider.GetDescriptor(template) + descriptorProvider := provider.NewCachedDescriptorProvider( + (&componentdescriptor.FakeService{}).Register(moduleTemplateFromFile.Spec.Descriptor.Raw)) - require.Error(t, err) - require.ErrorIs(t, err, provider.ErrDescriptorNil) + ocmi, err := ocmidentity.NewComponentId("kyma-project.io/module/template-operator", "1.0.0-new-ocm-format") + require.NoError(t, err) + + // when + desc, err := descriptorProvider.GetDescriptor(*ocmi) + + // then + require.NoError(t, err) + assert.Equal(t, ocmi.Name(), desc.Name) + assert.Equal(t, ocmi.Version(), desc.Version) } -func TestGetDescriptor_OnEmptyCache_ReturnsParsedDescriptor(t *testing.T) { - descriptorProvider := provider.NewCachedDescriptorProvider() - template := builder.NewModuleTemplateBuilder().Build() +func TestGetDescriptor_DoesNotUpdateCache(t *testing.T) { + // given + var moduleTemplateFromFile v1beta2.ModuleTemplate + builder.ReadComponentDescriptorFromFile("v1beta2_template_operator_new_ocm.yaml", &moduleTemplateFromFile) + + mockService := &componentdescriptor.FakeService{} + mockService.Register(moduleTemplateFromFile.Spec.Descriptor.Raw) - _, err := descriptorProvider.GetDescriptor(template) + descriptorProvider := provider.NewCachedDescriptorProvider(mockService) + ocmi, err := ocmidentity.NewComponentId("kyma-project.io/module/template-operator", "1.0.0-new-ocm-format") require.NoError(t, err) -} -func TestAdd_OnInvalidRawDescriptor_ReturnsErrDecode(t *testing.T) { - descriptorProvider := provider.NewCachedDescriptorProvider() - template := builder.NewModuleTemplateBuilder(). - WithVersion("1.0.0"). - WithRawDescriptor([]byte("invalid descriptor")). - WithDescriptor(nil). - Build() + // when + desc, err := descriptorProvider.GetDescriptor(*ocmi) - err := descriptorProvider.Add(template) + // then + require.NoError(t, err) + assert.Equal(t, ocmi.Name(), desc.Name) + assert.Equal(t, ocmi.Version(), desc.Version) + // and when + mockService.Clear().Register([]byte("invalid descriptor")) // make the service return junk data + _, err = descriptorProvider.GetDescriptor(*ocmi) // should come from the service, + // because the cache was not updated - and fail + + // then require.Error(t, err) - assert.Contains(t, err.Error(), provider.ErrDecode.Error()) + assert.ErrorIs(t, err, componentdescriptor.ErrDecode) } -func TestAdd_OnDescriptorTypeButNull_ReturnsNoError(t *testing.T) { - descriptorProvider := provider.NewCachedDescriptorProvider() - template := builder.NewModuleTemplateBuilder().WithVersion("1.0.0").WithDescriptor(&types.Descriptor{}).Build() +func TestGetDescriptor_ReturnsDescriptorFromCache(t *testing.T) { + // given + var moduleTemplateFromFile v1beta2.ModuleTemplate + builder.ReadComponentDescriptorFromFile("v1beta2_template_operator_new_ocm.yaml", &moduleTemplateFromFile) + mockService := &componentdescriptor.FakeService{} + mockService.Register(moduleTemplateFromFile.Spec.Descriptor.Raw) + descriptorProvider := provider.NewCachedDescriptorProvider(mockService) + ocmi, err := ocmidentity.NewComponentId("kyma-project.io/module/template-operator", "1.0.0-new-ocm-format") + require.NoError(t, err) + + err = descriptorProvider.Add(*ocmi) // add to cache + require.NoError(t, err) - err := descriptorProvider.Add(template) + // when + mockService.Clear().Register([]byte("invalid descriptor")) // make the service return junk data + descFromCache, err := descriptorProvider.GetDescriptor(*ocmi) // should come from the cache + // then require.NoError(t, err) + assert.Equal(t, ocmi.Name(), descFromCache.Name) + assert.Equal(t, ocmi.Version(), descFromCache.Version) } -func TestGetDescriptor_OnEmptyCache_AddsDescriptorFromTemplate(t *testing.T) { - descriptorCache := cache.NewDescriptorCache() - descriptorProvider := &provider.CachedDescriptorProvider{ - DescriptorCache: descriptorCache, - } +func TestGetDescriptorWithIdentity_WithNilProvider_ReturnsErr(t *testing.T) { + descriptorProvider := provider.NewCachedDescriptorProvider(nil) + _, err := descriptorProvider.GetDescriptorWithIdentity(nil) + require.Error(t, err) + require.ErrorIs(t, err, provider.ErrNilProvider) +} - expected := &types.Descriptor{ - ComponentDescriptor: compdesc.New("test-component", "1.0.0"), - } - template := builder.NewModuleTemplateBuilder().WithVersion("1.0.0").WithDescriptor(expected).Build() +func TestGetDescriptorWithIdentity_WithNilIdentity_ReturnsErr(t *testing.T) { + descriptorProvider := provider.NewCachedDescriptorProvider(nil) + _, err := descriptorProvider.GetDescriptorWithIdentity(&mockIdentityProvider{}) + require.Error(t, err) + require.ErrorIs(t, err, provider.ErrNilIdentity) +} - key := descriptorProvider.GenerateDescriptorKey(template.Name, template.Spec.Version) - entry := descriptorCache.Get(key) - assert.Nil(t, entry) +func TestGetDescriptorWithIdentity_WithProviderErr_ReturnsErr(t *testing.T) { + descriptorProvider := provider.NewCachedDescriptorProvider(nil) + expectedErr := errors.New("some error") + _, err := descriptorProvider.GetDescriptorWithIdentity( + &mockIdentityProvider{err: expectedErr}) + require.Error(t, err) + require.ErrorIs(t, err, expectedErr) +} - err := descriptorProvider.Add(template) - require.NoError(t, err) +func TestGetDescriptorWithIdentity_OnValidIdentity_ReturnsDescriptor(t *testing.T) { + // given + var moduleTemplateFromFile v1beta2.ModuleTemplate + builder.ReadComponentDescriptorFromFile("v1beta2_template_operator_new_ocm.yaml", &moduleTemplateFromFile) - result, err := descriptorProvider.GetDescriptor(template) + descriptorProvider := provider.NewCachedDescriptorProvider( + (&componentdescriptor.FakeService{}).Register(moduleTemplateFromFile.Spec.Descriptor.Raw)) + + ocmi, err := ocmidentity.NewComponentId("kyma-project.io/module/template-operator", "1.0.0-new-ocm-format") require.NoError(t, err) - assert.Equal(t, expected.Name, result.Name) + mockProvider := &mockIdentityProvider{ocmi: ocmi} + + // when + desc, err := descriptorProvider.GetDescriptorWithIdentity(mockProvider) - entry = descriptorCache.Get(key) - assert.NotNil(t, entry) - assert.Equal(t, expected.Name, entry.Name) + // then + require.NoError(t, err) + assert.Equal(t, ocmi.Name(), desc.Name) + assert.Equal(t, ocmi.Version(), desc.Version) } -func TestGenerateDescriptorCacheKey(t *testing.T) { - testCases := []struct { - name string - moduleName string - moduleVersion string - want string - }{ - { - name: "with valid module name and version", - moduleName: "name", - moduleVersion: "1.0.0", - want: "name:1.0.0", - }, - } +type mockIdentityProvider struct { + err error + ocmi *ocmidentity.ComponentId +} - providerInstance := provider.NewCachedDescriptorProvider() - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - got := providerInstance.GenerateDescriptorKey(tc.moduleName, tc.moduleVersion) - assert.Equal(t, tc.want, got) - }) +func (b *mockIdentityProvider) GetOCMIdentity() (*ocmidentity.ComponentId, error) { + if b.err != nil { + return nil, b.err } + return b.ocmi, nil } diff --git a/internal/descriptor/types/ocmidentity/ocmidentity.go b/internal/descriptor/types/ocmidentity/ocmidentity.go new file mode 100644 index 0000000000..99cfc2f494 --- /dev/null +++ b/internal/descriptor/types/ocmidentity/ocmidentity.go @@ -0,0 +1,38 @@ +package ocmidentity + +import ( + "errors" + "fmt" +) + +var ErrValueNotProvided = errors.New("value not provided") + +// ComponentId uniquely identifies an OCM ComponentId. +// See: https://ocm.software/docs/overview/important-terms/#component-identity +type ComponentId struct { + componentName string + componentVersion string +} + +// NewComponentId is a constructor that ensures that both name and version are provided. +func NewComponentId(name, version string) (*ComponentId, error) { + if name == "" { + return nil, fmt.Errorf("invalid component name: %w", ErrValueNotProvided) + } + if version == "" { + return nil, fmt.Errorf("invalid component version: %w", ErrValueNotProvided) + } + + return &ComponentId{ + componentName: name, + componentVersion: version, + }, nil +} + +func (c ComponentId) Name() string { + return c.componentName +} + +func (c ComponentId) Version() string { + return c.componentVersion +} diff --git a/internal/manifest/img/layer.go b/internal/manifest/img/layer.go index 7d0175c778..c5c47ce74b 100644 --- a/internal/manifest/img/layer.go +++ b/internal/manifest/img/layer.go @@ -43,13 +43,14 @@ type ( type Layers []Layer -func (l Layer) ConvertToImageSpec() (*v1beta2.ImageSpec, error) { +func (l Layer) ConvertToImageSpec(ociRepo string) (*v1beta2.ImageSpec, error) { ociImage, ok := l.LayerRepresentation.(*OCI) if !ok { return nil, fmt.Errorf("%w: not an OCIImage", ErrLayerParsing) } + return &v1beta2.ImageSpec{ - Repo: ociImage.Repo, + Repo: ociRepo, // Note: we override the repo here with an explicit value Name: ociImage.Name, Ref: ociImage.Ref, Type: v1beta2.RefTypeMetadata(ociImage.Type), diff --git a/internal/manifest/img/parse.go b/internal/manifest/img/parse.go index 740654815a..fc167aa625 100644 --- a/internal/manifest/img/parse.go +++ b/internal/manifest/img/parse.go @@ -58,7 +58,7 @@ func parseLayersByName(repo *genericocireg.RepositorySpec, descriptor *compdesc. layers := Layers{} for _, resource := range descriptor.Resources { access := resource.Access - var layerRepresentation LayerRepresentation + var ociRef *OCI spec, err := ocm.DefaultContext().AccessSpecForSpec(access) if err != nil { return nil, fmt.Errorf("failed to create spec for acccess: %w", err) @@ -75,11 +75,10 @@ func parseLayersByName(repo *genericocireg.RepositorySpec, descriptor *compdesc. if !ok { return nil, common.ErrTypeAssert } - layerRef, err := getOCIRef(repo, descriptor, accessSpec) + ociRef, err = getOCIRef(repo, descriptor, accessSpec) if err != nil { return nil, fmt.Errorf("building the digest url: %w", err) } - layerRepresentation = layerRef // this resource type is not relevant for module rendering but for security scanning only case ociartifact.Type: fallthrough @@ -99,7 +98,7 @@ func parseLayersByName(repo *genericocireg.RepositorySpec, descriptor *compdesc. layers = append( layers, Layer{ LayerName: v1beta2.LayerName(resource.Name), - LayerRepresentation: layerRepresentation, + LayerRepresentation: ociRef, }, ) } @@ -141,6 +140,7 @@ func getOCIRef( if repo.SubPath != "" { baseURL = fmt.Sprintf("%s/%s", repo.Name(), repo.SubPath) } + layerRef.Repo = baseURL + "/" layerRef.Name = sha256sum(descriptor.GetName()) default: diff --git a/internal/manifest/img/parse_test.go b/internal/manifest/img/parse_test.go index e8382edf0c..ef2b2a8059 100644 --- a/internal/manifest/img/parse_test.go +++ b/internal/manifest/img/parse_test.go @@ -7,7 +7,9 @@ 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/descriptor/types/ocmidentity" "github.com/kyma-project/lifecycle-manager/internal/manifest/img" + "github.com/kyma-project/lifecycle-manager/internal/service/componentdescriptor" "github.com/kyma-project/lifecycle-manager/pkg/testutils" "github.com/kyma-project/lifecycle-manager/pkg/testutils/builder" ) @@ -15,12 +17,14 @@ import ( func TestParse(t *testing.T) { tests := []struct { name string - DescriptorSourceFile string + descriptorSourceFile string + descriptorVersion string want img.Layer }{ { "should parse raw-manifest layer from mediaType: application/x-tar", "v1beta2_template_operator_new_ocm.yaml", + "1.0.0-new-ocm-format", img.Layer{ LayerName: "raw-manifest", LayerRepresentation: &img.OCI{ @@ -33,6 +37,7 @@ func TestParse(t *testing.T) { }, { "should parse raw-manifest layer from mediaType: application/octet-stream", "v1beta2_template_operator_current_ocm.yaml", + "1.1.1-e2e-test", img.Layer{ LayerName: "raw-manifest", LayerRepresentation: &img.OCI{ @@ -47,9 +52,14 @@ func TestParse(t *testing.T) { for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { var moduleTemplateFromFile v1beta2.ModuleTemplate - builder.ReadComponentDescriptorFromFile(testCase.DescriptorSourceFile, + builder.ReadComponentDescriptorFromFile(testCase.descriptorSourceFile, &moduleTemplateFromFile) - descriptor, err := provider.NewCachedDescriptorProvider().GetDescriptor(&moduleTemplateFromFile) + ocmi, err := ocmidentity.NewComponentId( + "kyma-project.io/module/template-operator", testCase.descriptorVersion) + require.NoError(t, err) + descriptor, err := provider.NewCachedDescriptorProvider( + componentdescriptor.NewFakeService(moduleTemplateFromFile.Spec.Descriptor.Raw)). + GetDescriptor(*ocmi) require.NoError(t, err) layers, err := img.Parse(descriptor.ComponentDescriptor) require.NoError(t, err) diff --git a/internal/manifest/img/pathextractor.go b/internal/manifest/img/pathextractor.go index daaa642555..af64dad6ec 100644 --- a/internal/manifest/img/pathextractor.go +++ b/internal/manifest/img/pathextractor.go @@ -16,6 +16,7 @@ import ( "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/crane" containerregistryv1 "github.com/google/go-containerregistry/pkg/v1" + "ocm.software/ocm/api/ocm/extensions/repositories/genericocireg/componentmapping" "github.com/kyma-project/lifecycle-manager/api/v1beta2" "github.com/kyma-project/lifecycle-manager/internal/manifest/filemutex" @@ -65,7 +66,9 @@ func (p PathExtractor) GetPathForFetchedLayer(ctx context.Context, keyChain authn.Keychain, filename string, ) (string, error) { - imageRef := fmt.Sprintf("%s/%s@%s", imageSpec.Repo, imageSpec.Name, imageSpec.Ref) + imageRef := fmt.Sprintf("%s/%s/%s@%s", imageSpec.Repo, componentmapping.ComponentDescriptorNamespace, + imageSpec.Name, imageSpec.Ref, + ) installPath := getFsChartPath(imageSpec) manifestPath := path.Join(installPath, filename) diff --git a/internal/manifest/img/pathextractor_test.go b/internal/manifest/img/pathextractor_test.go index fdcde55563..fd00c18894 100644 --- a/internal/manifest/img/pathextractor_test.go +++ b/internal/manifest/img/pathextractor_test.go @@ -66,6 +66,8 @@ func TestPathExtractor_ExtractLayer(t *testing.T) { } func TestPathExtractor_FetchLayerToFile(t *testing.T) { + const commonRepo = "europe-west3-docker.pkg.dev/sap-kyma-jellyfish-dev/template-operator/component-descriptors" + tests := []struct { name string fileName string @@ -77,7 +79,8 @@ func TestPathExtractor_FetchLayerToFile(t *testing.T) { img.Layer{ LayerName: "raw-manifest", LayerRepresentation: &img.OCI{ - Repo: "europe-west3-docker.pkg.dev/sap-kyma-jellyfish-dev/template-operator/component-descriptors", + Repo: "normally-determined-by-OCM-component-descriptor-but-" + + "in-our-code-is-overridden-with-an-explicit-value", Name: testutils.DefaultFQDN, Ref: "sha256:d2cc278224a71384b04963a83e784da311a268a2b3fa8732bc31e70ca0c5bc52", Type: "oci-dir", @@ -90,7 +93,8 @@ func TestPathExtractor_FetchLayerToFile(t *testing.T) { img.Layer{ LayerName: "raw-manifest", LayerRepresentation: &img.OCI{ - Repo: "europe-west3-docker.pkg.dev/sap-kyma-jellyfish-dev/template-operator/component-descriptors", + Repo: "normally-determined-by-OCM-component-descriptor-but-" + + "in-our-code-is-overridden-with-an-explicit-value", Name: testutils.DefaultFQDN, Ref: "sha256:1ea2baf45791beafabfee533031b715af8f7a4ffdfbbf30d318f52f7652c36ca", Type: "oci-ref", @@ -101,7 +105,7 @@ func TestPathExtractor_FetchLayerToFile(t *testing.T) { for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { p := img.NewPathExtractor() - imageSpec, err := testCase.want.ConvertToImageSpec() + imageSpec, err := testCase.want.ConvertToImageSpec(commonRepo) require.NoError(t, err) extractedFilePath, err := p.GetPathFromRawManifest(t.Context(), *imageSpec, authn.DefaultKeychain) require.NoError(t, err) diff --git a/internal/manifest/parser/template_to_module.go b/internal/manifest/parser/template_to_module.go index f11158a711..21bdc7d389 100644 --- a/internal/manifest/parser/template_to_module.go +++ b/internal/manifest/parser/template_to_module.go @@ -19,23 +19,29 @@ import ( "github.com/kyma-project/lifecycle-manager/pkg/templatelookup" ) -var ErrConvertingToOCIAccessSpec = errors.New("failed converting resource.AccessSpec to *ociartifact.AccessSpec") +var ( + ErrConvertingToOCIAccessSpec = errors.New("failed converting resource.AccessSpec to *ociartifact.AccessSpec") + ErrConvertingToImgOCI = errors.New("failed converting layerRepresentation to *img.OCI") +) type Parser struct { client.Client descriptorProvider *provider.CachedDescriptorProvider remoteSyncNamespace string + ociRepo string } func NewParser(clnt client.Client, descriptorProvider *provider.CachedDescriptorProvider, remoteSyncNamespace string, + ociRepo string, ) *Parser { return &Parser{ Client: clnt, descriptorProvider: descriptorProvider, remoteSyncNamespace: remoteSyncNamespace, + ociRepo: ociRepo, } } @@ -83,7 +89,7 @@ func (p *Parser) appendModuleWithInformation(module templatelookup.ModuleInfo, k }) return modules } - descriptor, err := p.descriptorProvider.GetDescriptor(template.ModuleTemplate) + descriptor, err := p.descriptorProvider.GetDescriptorWithIdentity(template) if err != nil { template.Err = err modules = append(modules, &modulecommon.Module{ @@ -98,8 +104,8 @@ func (p *Parser) appendModuleWithInformation(module templatelookup.ModuleInfo, k name := modulecommon.CreateModuleName(fqdn, kyma.Name, module.Name) setNameAndNamespaceIfEmpty(template, name, p.remoteSyncNamespace) var manifest *v1beta2.Manifest - if manifest, err = p.newManifestFromTemplate(module.Module, - template.ModuleTemplate); err != nil { + if manifest, err = newManifestFromTemplate(module.Module, + template.ModuleTemplate, descriptor, p.ociRepo); err != nil { template.Err = err modules = append(modules, &modulecommon.Module{ ModuleName: module.Name, @@ -138,9 +144,11 @@ func setNameAndNamespaceIfEmpty(template *templatelookup.ModuleTemplateInfo, nam } } -func (p *Parser) newManifestFromTemplate( +func newManifestFromTemplate( module v1beta2.Module, template *v1beta2.ModuleTemplate, + descriptor *types.Descriptor, + repo string, ) (*v1beta2.Manifest, error) { manifest := &v1beta2.Manifest{} if manifest.Annotations == nil { @@ -154,16 +162,12 @@ func (p *Parser) newManifestFromTemplate( var layers img.Layers var err error - descriptor, err := p.descriptorProvider.GetDescriptor(template) - if err != nil { - return nil, fmt.Errorf("failed to get descriptor from template: %w", err) - } if layers, err = img.Parse(descriptor.ComponentDescriptor); err != nil { return nil, fmt.Errorf("could not parse descriptor: %w", err) } - if err := translateLayersAndMergeIntoManifest(manifest, layers); err != nil { + if err := translateLayersAndMergeIntoManifest(manifest, layers, repo); err != nil { return nil, fmt.Errorf("could not translate layers and merge them: %w", err) } @@ -203,27 +207,44 @@ func getLocalizedImagesFromDescriptor(descriptor *types.Descriptor) []string { return localizedImages } -func translateLayersAndMergeIntoManifest(manifest *v1beta2.Manifest, layers img.Layers) error { +func translateLayersAndMergeIntoManifest(manifest *v1beta2.Manifest, layers img.Layers, repo string) error { for _, layer := range layers { - if err := insertLayerIntoManifest(manifest, layer); err != nil { + if err := insertLayerIntoManifest(manifest, layer, repo); err != nil { return fmt.Errorf("error in layer %s: %w", layer.LayerName, err) } } return nil } -func insertLayerIntoManifest(manifest *v1beta2.Manifest, layer img.Layer) error { +func insertLayerIntoManifest(manifest *v1beta2.Manifest, layer img.Layer, ociRepoFromConfig string) error { switch layer.LayerName { case v1beta2.DefaultCRLayer: // default CR layer is not relevant for the manifest case v1beta2.ConfigLayer: - imageSpec, err := layer.ConvertToImageSpec() + imageSpec, err := layer.ConvertToImageSpec(ociRepoFromConfig) if err != nil { return fmt.Errorf("error while parsing config layer: %w", err) } manifest.Spec.Config = imageSpec case v1beta2.RawManifestLayer: - installRaw, err := layer.ToInstallRaw() + ociImage, ok := layer.LayerRepresentation.(*img.OCI) + if !ok { + return fmt.Errorf("%w: actual type: %T", ErrConvertingToImgOCI, layer.LayerRepresentation) + } + + // For fetching data from the OCI registry use the repo from the global config + // instead of the one from the layer (it is the same as in the ComponentDescriptor). + // These two values may be different and the explicitly configured one is safer to use, + // as it is known to be reachable. + // After all, we've been able to read the ComponentDescriptor using it. + ociImageCopy := img.OCI{ + Repo: ociRepoFromConfig, + Name: ociImage.Name, + Ref: ociImage.Ref, + Type: ociImage.Type, + } + + installRaw, err := ociImageCopy.ToInstallRaw() if err != nil { return fmt.Errorf("error while merging the generic install representation: %w", err) } diff --git a/internal/repository/oci/export_test.go b/internal/repository/oci/export_test.go new file mode 100644 index 0000000000..2d8000ce73 --- /dev/null +++ b/internal/repository/oci/export_test.go @@ -0,0 +1,6 @@ +package oci + +// compiled only when running tests. +func SetCraneWrapper(r *RepositoryReader, cWrap craneWrapper) { + r.cWrapper = cWrap +} diff --git a/internal/repository/oci/ocirepo.go b/internal/repository/oci/ocirepo.go new file mode 100644 index 0000000000..8f98cb61b8 --- /dev/null +++ b/internal/repository/oci/ocirepo.go @@ -0,0 +1,121 @@ +package oci + +import ( + "context" + "errors" + "fmt" + "path" + "strings" + + "github.com/google/go-containerregistry/pkg/crane" + containerregistryv1 "github.com/google/go-containerregistry/pkg/v1" + + "github.com/kyma-project/lifecycle-manager/internal/manifest/spec" +) + +var ( + ErrKeyChainNotNil = errors.New("keychain lookup must not be nil") + ErrNoProtocolScheme = errors.New("hostref must not contain protocol scheme (http/https)") + ErrNoLeadingSlash = errors.New("hostref must not start with a '/'") +) + +// RepositoryReader provides basic support to read data from OCI repositories. +type RepositoryReader struct { + keyChainLookup spec.KeyChainLookup + hostref string + insecure bool + cWrapper craneWrapper // in runtime delegates to crane package functions +} + +// NewRepository creates a new RepositoryReader for the given hostref. +// If insecure is false, a non-nil KeyChainLookup must be provided to retrieve authentication information. +// The hostref must not contain a protocol scheme (http/https), for example: "k3d-kcp-registry.localhost:5000". +func NewRepository(kcl spec.KeyChainLookup, hostref string, insecure bool) (*RepositoryReader, error) { + if !insecure && kcl == nil { + return nil, ErrKeyChainNotNil + } + + if strings.Contains(hostref, "://") { + return nil, fmt.Errorf("%w: %q", ErrNoProtocolScheme, hostref) + } + + if strings.HasPrefix(hostref, "/") { + return nil, fmt.Errorf("%w: %q", ErrNoLeadingSlash, hostref) + } + + return &RepositoryReader{ + keyChainLookup: kcl, + hostref: hostref, + insecure: insecure, + cWrapper: &defaultCraneWrapper{}, + }, nil +} + +// GetConfigFile retrieves the OCI artifact config file as a byte slice. +// We're not using image-oriented types here because OCM artifacts "config file" is not a standard image config. +func (s *RepositoryReader) GetConfigFile(ctx context.Context, name, tag string) ([]byte, error) { + options, err := s.stdOptions(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get standard options: %w", err) + } + ref := s.toImageRef(name, tag) + configBytes, err := s.cWrapper.Config(ref, options...) + if err != nil { + return nil, fmt.Errorf("failed to get config file for ref=%q: %w", ref, err) + } + + return configBytes, nil +} + +// PullLayer retrieves a layer with given digest from an OCI artifact identified by name and tag. +func (s *RepositoryReader) PullLayer(ctx context.Context, name, tag, digest string) (containerregistryv1.Layer, error) { + options, err := s.stdOptions(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get standard options: %w", err) + } + ref := s.toImageRef(name, tag) + refWithDigest := fmt.Sprintf("%s@%s", ref, digest) + configBytes, err := s.cWrapper.PullLayer(refWithDigest, options...) + if err != nil { + return nil, fmt.Errorf("failed to pull layer for ref=%q: %w", refWithDigest, err) + } + return configBytes, nil +} + +func (s *RepositoryReader) toImageRef(name, tag string) string { + hostPath := path.Join(s.hostref, "component-descriptors", name) + return fmt.Sprintf("%s:%s", hostPath, tag) +} + +func (s *RepositoryReader) stdOptions(ctx context.Context) ([]crane.Option, error) { + options := []crane.Option{crane.WithContext(ctx)} + + keyChain, err := s.keyChainLookup.Get(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get keychain: %w", err) + } + options = append(options, crane.WithAuthFromKeychain(keyChain)) + + if s.insecure { + options = append(options, crane.Insecure) + } + + return options, nil +} + +// craneWrapper is a subset of crane package functions used by RepositoryReader. +// It is introduced to facilitate testing. +type craneWrapper interface { + Config(ref string, opt ...crane.Option) ([]byte, error) + PullLayer(ref string, opt ...crane.Option) (containerregistryv1.Layer, error) +} + +type defaultCraneWrapper struct{} + +func (c *defaultCraneWrapper) Config(ref string, opt ...crane.Option) ([]byte, error) { + return crane.Config(ref, opt...) //nolint:wrapcheck // the crane wrapper should be transparent +} + +func (c *defaultCraneWrapper) PullLayer(ref string, opt ...crane.Option) (containerregistryv1.Layer, error) { + return crane.PullLayer(ref, opt...) //nolint:wrapcheck // the crane wrapper should be transparent +} diff --git a/internal/repository/oci/ocirepo_test.go b/internal/repository/oci/ocirepo_test.go new file mode 100644 index 0000000000..613e3beb47 --- /dev/null +++ b/internal/repository/oci/ocirepo_test.go @@ -0,0 +1,238 @@ +package oci_test + +import ( + "context" + "errors" + "testing" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/crane" + containerregistryv1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/static" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/kyma-project/lifecycle-manager/internal/manifest/spec" + "github.com/kyma-project/lifecycle-manager/internal/repository/oci" +) + +const ( + commonOCIArtifactRef = "europe-docker.pkg.dev/kyma-project/prod/component-descriptors/" +) + +func TestNewRepository(t *testing.T) { + tests := []struct { + name string + hostPort string + insecure bool + kcl spec.KeyChainLookup + expectErr bool + }{ + { + name: "valid with insecure", + hostPort: "example.com:5000", + insecure: true, + kcl: nil, + expectErr: false, + }, + { + name: "valid without insecure and with keychain", + hostPort: "example.com:5000", + insecure: false, + kcl: &mockKeyChainLookup{}, + expectErr: false, + }, + { + name: "invalid: secure and without keychain", + hostPort: "example.com:5000", + insecure: false, + kcl: nil, + expectErr: true, + }, + { + name: "invalid with protocol in hostPort", + hostPort: "https://example.com:5000", + insecure: true, + kcl: nil, + expectErr: true, + }, + { + name: "invalid with leading slash in hostPort", + hostPort: "/example.com:5000", + insecure: true, + kcl: nil, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := oci.NewRepository(tt.kcl, tt.hostPort, tt.insecure) + if (err != nil) != tt.expectErr { + t.Errorf("NewRepository() error = %v, expectErr %v", err, tt.expectErr) + } + }) + } +} + +func TestGetConfigFile(t *testing.T) { + t.Run("should fetch config file successfully", func(t *testing.T) { + // given + mcc := mockCraneClient{ + configResult: []byte("mock config data"), + } + repo, err := oci.NewRepository(&mockKeyChainLookup{}, "europe-docker.pkg.dev/kyma-project/prod", true) + require.NoError(t, err) + oci.SetCraneWrapper(repo, &mcc) + // when + configData, err := repo.GetConfigFile(t.Context(), "test-name", "test-version") + // then + require.NoError(t, err) + assert.Equal(t, []byte("mock config data"), configData) + assert.Equal(t, commonOCIArtifactRef+"test-name:test-version", + mcc.configRefArg, + ) + + opts := mcc.configOptsArg + assert.Len(t, opts, 3) // one for context, one for keychain, one for insecure + }) + + t.Run("should return an error from KeyChain Lookup", func(t *testing.T) { + // given + repo, err := oci.NewRepository(&errorKeyChainLookup{}, "dummy", false) + require.NoError(t, err) + // when + _, err = repo.GetConfigFile(t.Context(), "test-name", "test-version") + // then + require.Error(t, err) + require.ErrorIs(t, err, errKeyChain) + assert.Contains(t, err.Error(), "failed to get keychain:") + }) + + t.Run("should use provided keychain lookup when secure", func(t *testing.T) { + // given + mcc := mockCraneClient{ + configResult: []byte("mock config data"), + } + mkcl := &mockKeyChainLookup{} + repo, err := oci.NewRepository(mkcl, "europe-docker.pkg.dev/kyma-project/prod", false) + require.NoError(t, err) + oci.SetCraneWrapper(repo, &mcc) + assert.Nil(t, mkcl.ctx) + // when + configData, err := repo.GetConfigFile(t.Context(), "test-name", "test-version") + // then + require.NoError(t, err) + assert.Equal(t, []byte("mock config data"), configData) + // ensure that the keychain lookup was called (and so used) with the given context: + assert.Equal(t, t.Context(), mkcl.ctx) + }) + + t.Run("should return an error from craneClient.Config", func(t *testing.T) { + // given + repo, err := oci.NewRepository(&mockKeyChainLookup{}, "europe-docker.pkg.dev/kyma-project/prod", true) + require.NoError(t, err) + oci.SetCraneWrapper(repo, &mockCraneClient{}) + // when + _, err = repo.GetConfigFile(t.Context(), "test-name", "test-version") + // then + require.Error(t, err) + require.ErrorIs(t, err, errMockConfig) + assert.Contains(t, err.Error(), "failed to get config file for ref=") + assert.Contains(t, err.Error(), commonOCIArtifactRef+ + "test-name:test-version", + ) + }) +} + +func TestPullLayer(t *testing.T) { + t.Run("should pull layer successfully", func(t *testing.T) { + mockLayer := static.NewLayer([]byte("mock layer data"), "application/mock") + mcc := mockCraneClient{ + pullResult: mockLayer, + } + + repo, err := oci.NewRepository(&mockKeyChainLookup{}, "europe-docker.pkg.dev/kyma-project/prod", true) + require.NoError(t, err) + oci.SetCraneWrapper(repo, &mcc) + + layer, err := repo.PullLayer(t.Context(), "test-name", "test-version", "sha256:abcdef1234567890") + require.NoError(t, err) + assert.Equal(t, mockLayer, layer) + assert.Equal(t, commonOCIArtifactRef+"test-name:test-version@sha256:abcdef1234567890", + mcc.pullRefArg, + ) + + opts := mcc.pullOptsArg + assert.Len(t, opts, 3) // one for context, one for keychain, one for insecure + }) + + t.Run("should return an error from KeyChain Lookup", func(t *testing.T) { + repo, err := oci.NewRepository(&errorKeyChainLookup{}, "europe-docker.pkg.dev/kyma-project/prod", false) + require.NoError(t, err) + _, err = repo.PullLayer(t.Context(), "test-name", "test-version", "sha256:abcdef1234567890") + require.Error(t, err) + assert.ErrorIs(t, err, errKeyChain) + }) + + t.Run("should return an error from craneClient.PullLayer", func(t *testing.T) { + repo, err := oci.NewRepository(&mockKeyChainLookup{}, "europe-docker.pkg.dev/kyma-project/prod", true) + require.NoError(t, err) + oci.SetCraneWrapper(repo, &mockCraneClient{}) + _, err = repo.PullLayer(t.Context(), "test-name", "test-version", "sha256:abcdef1234567890") + require.Error(t, err) + require.ErrorIs(t, err, errMockPull) + assert.Contains(t, err.Error(), "failed to pull layer for ref=") + assert.Contains(t, err.Error(), commonOCIArtifactRef+"test-name:test-version@sha256:abcdef1234567890") + }) +} + +var ( + errMockConfig = errors.New("mock config error") + errMockPull = errors.New("mock pull error") +) + +type mockCraneClient struct { + configRefArg string + configOptsArg []crane.Option + configResult []byte + + pullRefArg string + pullOptsArg []crane.Option + pullResult containerregistryv1.Layer +} + +func (m *mockCraneClient) Config(ref string, opts ...crane.Option) ([]byte, error) { + m.configRefArg = ref + m.configOptsArg = opts + if m.configResult == nil { + return nil, errMockConfig + } + return m.configResult, nil +} + +func (m *mockCraneClient) PullLayer(ref string, opt ...crane.Option) (containerregistryv1.Layer, error) { + m.pullRefArg = ref + m.pullOptsArg = opt + if m.pullResult == nil { + return nil, errMockPull + } + return m.pullResult, nil +} + +var errKeyChain = errors.New("keychain error") + +type errorKeyChainLookup struct{} + +func (e *errorKeyChainLookup) Get(ctx context.Context) (authn.Keychain, error) { + return nil, errKeyChain +} + +type mockKeyChainLookup struct { + ctx context.Context //nolint:containedctx //used to verify that the mock is called with the correct context +} + +func (m *mockKeyChainLookup) Get(ctx context.Context) (authn.Keychain, error) { + m.ctx = ctx + return nil, nil +} diff --git a/internal/service/componentdescriptor/extractfile_internal_test.go b/internal/service/componentdescriptor/extractfile_internal_test.go new file mode 100644 index 0000000000..c474bdb4a2 --- /dev/null +++ b/internal/service/componentdescriptor/extractfile_internal_test.go @@ -0,0 +1,82 @@ +package componentdescriptor + +import ( + "bytes" + "errors" + "io" + "testing" + + containerregistryv1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestExtractFile(t *testing.T) { + t.Run("should return valid error when layer content is empty", func(t *testing.T) { + fileName := "someReallyEmptyfile" + res, err := defaultFileExtractor().extractFileFromLayer(&mockLayer{}, fileName) + require.Nil(t, res) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to extract data of file=\""+fileName+"\" from TAR archive") + assert.Contains(t, err.Error(), "layer content is empty") + }) + + t.Run("should preserve original error when calling layer.Uncompressed", func(t *testing.T) { + expectedErr := errors.New("error from Uncompressed") + mockLayer := &mockLayer{ + errOnUncompressed: expectedErr, + } + res, err := defaultFileExtractor().extractFileFromLayer(mockLayer, "somefile") + require.Nil(t, res) + require.Error(t, err) + assert.ErrorIs(t, err, expectedErr) + }) + + t.Run("should preserve original error when calling layer.Digest", func(t *testing.T) { + expectedErr := errors.New("error from Digest") + mockLayer := &mockLayer{ + errOnDigest: expectedErr, + } + res, err := defaultFileExtractor().extractFileFromLayer(mockLayer, "somefile") + require.Nil(t, res) + require.Error(t, err) + assert.Contains(t, err.Error(), "from TAR archive in a layer with digest") + assert.Contains(t, err.Error(), "somefile") + assert.ErrorIs(t, err, expectedErr) + }) + + t.Run("should preserve original error when calling ReadAll", func(t *testing.T) { + expectedErr := errors.New("error from ReadAll") + subject := defaultFileExtractor() + subject.readAll = func(r io.Reader) ([]byte, error) { + return nil, expectedErr + } + res, err := subject.extractFileFromLayer(&mockLayer{}, "somefile") + require.Nil(t, res) + require.Error(t, err) + assert.ErrorIs(t, err, expectedErr) + }) +} + +type mockLayer struct { + containerregistryv1.Layer + + errOnUncompressed error + errOnDigest error +} + +func (m *mockLayer) Uncompressed() (io.ReadCloser, error) { + if m.errOnUncompressed != nil { + return nil, m.errOnUncompressed + } + emptyReader := bytes.NewReader([]byte{}) + // return a non-nil ReadCloser with empty content + return io.NopCloser(emptyReader), nil +} + +func (m *mockLayer) Digest() (containerregistryv1.Hash, error) { + if m.errOnDigest != nil { + return containerregistryv1.Hash{}, m.errOnDigest + } + return containerregistryv1.Hash{Algorithm: "foo", Hex: "bar"}, nil +} diff --git a/internal/service/componentdescriptor/extractor.go b/internal/service/componentdescriptor/extractor.go new file mode 100644 index 0000000000..b026f0baf6 --- /dev/null +++ b/internal/service/componentdescriptor/extractor.go @@ -0,0 +1,62 @@ +package componentdescriptor + +import ( + "errors" + "fmt" + "io" + + containerregistryv1 "github.com/google/go-containerregistry/pkg/v1" +) + +// fileExtractor is responsible for extracting a specific file from a container image layer. +type fileExtractor struct { + // reads all data from the provided reader + readAll func(reader io.Reader) ([]byte, error) + + // extracts the file with expectedName from the given tarArchive and returns its content + unTar func(tarInput []byte, expectedName string) ([]byte, error) +} + +func (fExt *fileExtractor) extractFileFromLayer(layer containerregistryv1.Layer, fileName string) ([]byte, error) { + wrap := func(err error) error { + digest, derr := layer.Digest() + if derr != nil { + err = errors.Join(err, derr) + } + return fmt.Errorf("failed to extract data of file=%q from TAR archive in a layer with digest=%q: %w", + fileName, digest, err) + } + + layerReader, err := layer.Uncompressed() + if err != nil { + return nil, wrap(err) + } + defer layerReader.Close() + + layerBytes, err := fExt.readAll(layerReader) + if err != nil { + return nil, wrap(err) + } + + if len(layerBytes) == 0 { + return nil, wrap(ErrLayerEmpty) + } + + compdescBytes, err := fExt.unTar(layerBytes, fileName) + if err != nil { + return nil, wrap(err) + } + + return compdescBytes, nil +} + +func defaultFileExtractor() *fileExtractor { + unTar := func(tarInput []byte, expectedName string) ([]byte, error) { + return defaultTarExtractor(tarInput).unTar(expectedName) + } + + return &fileExtractor{ + readAll: io.ReadAll, + unTar: unTar, + } +} diff --git a/internal/service/componentdescriptor/fakeservice.go b/internal/service/componentdescriptor/fakeservice.go new file mode 100644 index 0000000000..e9ad8ab1f3 --- /dev/null +++ b/internal/service/componentdescriptor/fakeservice.go @@ -0,0 +1,127 @@ +package componentdescriptor + +import ( + "context" + "fmt" + + "github.com/kyma-project/lifecycle-manager/internal/descriptor/types" + "github.com/kyma-project/lifecycle-manager/internal/descriptor/types/ocmidentity" +) + +// Fake Service implementation for tests requiring component descriptors. +// It supports registering descriptors with name and version overrides - useful for scenarios +// where a single descriptor is used for multiple test cases with slightly different module names. +// Defining this fake here has the advantage of using the same internal +// deserialization logic as the real service, which makes this fake a bit more "real". +type FakeService struct { + registeredDescriptors []registeredDescriptor + stopped bool +} + +func NewFakeService(descBytes []byte) *FakeService { + return (&FakeService{}).Register(descBytes) +} + +func (s *FakeService) GetComponentDescriptor(ctx context.Context, ocmi ocmidentity.ComponentId) ( + *types.Descriptor, error, +) { + if s.stopped { + panic("cannot get from a stopped FakeService") + } + + for _, entry := range s.registeredDescriptors { + if entry.withOverride { + if entry.nameOverride == ocmi.Name() && entry.versionOverride == ocmi.Version() { + // user is asking for the descriptor + // that was registered with specific name and version override + return s.deserializeOverride(entry, ocmi) + } + } else { + // check if the registered descriptor matches the requested name and version + result, err := deserialize(entry.rawDesc, ocmi) + if err != nil { + return nil, err + } + if result.Name == ocmi.Name() && result.Version == ocmi.Version() { + return &types.Descriptor{ + ComponentDescriptor: result, + }, nil + } + } + } + + //nolint: err113 // it's only used in tests, there is no point in making it a public API error type + notFoundErr := fmt.Errorf("component descriptor with name: %q and version %q not found", + ocmi.Name(), + ocmi.Version(), + ) + + return nil, notFoundErr +} + +func (ts *FakeService) Clear() *FakeService { + if ts.stopped { + panic("cannot clear a stopped FakeService") + } + ts.registeredDescriptors = nil + return ts +} + +func (ts *FakeService) Register(descBytes []byte) *FakeService { + if ts.stopped { + panic("cannot register to a stopped FakeService") + } + registered := registeredDescriptor{ + rawDesc: descBytes, + } + ts.registeredDescriptors = append(ts.registeredDescriptors, registered) + return ts +} + +func (ts *FakeService) RegisterWithNameVersionOverride(name, version string, descBytes []byte) *FakeService { + if ts.stopped { + panic("cannot register to a stopped FakeService") + } + registered := registeredDescriptor{ + withOverride: true, + nameOverride: name, + versionOverride: version, + rawDesc: descBytes, + } + + ts.registeredDescriptors = append(ts.registeredDescriptors, registered) + return ts +} + +func (fs *FakeService) Stop() { + fs.stopped = true +} + +func (fs *FakeService) Resume() bool { + if fs.stopped { + fs.stopped = false + return true + } + return false +} + +func (s *FakeService) deserializeOverride(entry registeredDescriptor, ocmi ocmidentity.ComponentId) ( + *types.Descriptor, error, +) { + result, err := deserialize(entry.rawDesc, ocmi) + if err != nil { + return nil, err + } + result.Name = ocmi.Name() + result.Version = ocmi.Version() + return &types.Descriptor{ + ComponentDescriptor: result, + }, nil +} + +type registeredDescriptor struct { + withOverride bool + nameOverride string + versionOverride string + rawDesc []byte +} diff --git a/internal/service/componentdescriptor/service.go b/internal/service/componentdescriptor/service.go new file mode 100644 index 0000000000..6b292ab1ee --- /dev/null +++ b/internal/service/componentdescriptor/service.go @@ -0,0 +1,123 @@ +package componentdescriptor + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + containerregistryv1 "github.com/google/go-containerregistry/pkg/v1" + "ocm.software/ocm/api/ocm/compdesc" + "ocm.software/ocm/api/ocm/extensions/repositories/genericocireg" + + "github.com/kyma-project/lifecycle-manager/internal/descriptor/types" + "github.com/kyma-project/lifecycle-manager/internal/descriptor/types/ocmidentity" +) + +const ( + ComponentDescriptorFileName = compdesc.ComponentDescriptorFileName +) + +var ( + ErrInvalidArg = errors.New("invalid argument") + ErrLayerNil = errors.New("ComponentDescriptorLayer is nil in ComponentDescriptorConfig") + ErrLayerDigestEmpty = errors.New("ComponentDescriptorLayer.Digest is empty in ComponentDescriptorConfig") + ErrNotFoundInTar = errors.New("not found in TAR archive") + ErrTarTooLarge = errors.New("entry in the TAR archive is too large") + ErrLayerEmpty = errors.New("layer content is empty") + ErrDecode = errors.New("failed to decode component descriptor") +) + +type OCIRepository interface { + GetConfigFile(ctx context.Context, name, tag string) ([]byte, error) + PullLayer(ctx context.Context, name, tag, digest string) (containerregistryv1.Layer, error) +} + +type Service struct { + ociRepository OCIRepository + extractFileFromLayer func(layer containerregistryv1.Layer, fileName string) ([]byte, error) +} + +func NewService(ociRepository OCIRepository) (*Service, error) { + if ociRepository == nil { + return nil, fmt.Errorf("ociRepository must not be nil: %w", ErrInvalidArg) + } + + return &Service{ + ociRepository: ociRepository, + extractFileFromLayer: defaultFileExtractor().extractFileFromLayer, + }, nil +} + +func commonErrMsg(ocmi ocmidentity.ComponentId) string { + return fmt.Sprintf("ocm artifact with name=%q and version=%q", + ocmi.Name(), ocmi.Version()) +} + +func (s *Service) GetComponentDescriptor(ctx context.Context, ocmi ocmidentity.ComponentId) (*types.Descriptor, error) { + compDescLayerDigest, err := s.getDescriptorLayerDigest(ctx, ocmi) + if err != nil { + return nil, err + } + + layer, err := s.ociRepository.PullLayer(ctx, ocmi.Name(), ocmi.Version(), compDescLayerDigest) + if err != nil { + return nil, fmt.Errorf("failed to pull layer for ocm artifact with name=%q, version=%q and digest=%q: %w", + ocmi.Name(), ocmi.Version(), compDescLayerDigest, err) + } + + compdescBytes, err := s.extractFileFromLayer(layer, ComponentDescriptorFileName) + if err != nil { + return nil, + fmt.Errorf("failed to extract component descriptor from layer fetched from %s with digest=%q: %w", + commonErrMsg(ocmi), compDescLayerDigest, err) + } + + descriptor, err := deserialize(compdescBytes, ocmi) + if err != nil { + return nil, err + } + + return &types.Descriptor{ + ComponentDescriptor: descriptor, + }, nil +} + +// getDescriptorLayerDigest retrieves the digest of the ComponentDescriptor layer. +func (s *Service) getDescriptorLayerDigest(ctx context.Context, ocmi ocmidentity.ComponentId) (string, error) { + // Fetch the image config to get the ComponentDescriptor layer info + configBytes, err := s.ociRepository.GetConfigFile(ctx, ocmi.Name(), ocmi.Version()) + if err != nil { + return "", fmt.Errorf("failed to get config file for %s: %w", commonErrMsg(ocmi), err) + } + + ocmArtifactConfig := genericocireg.ComponentDescriptorConfig{} + err = json.Unmarshal(configBytes, &ocmArtifactConfig) + if err != nil { + return "", + fmt.Errorf("failed to unmarshal config data into ComponentDescriptorConfig for %s: %w", + commonErrMsg(ocmi), err) + } + + if ocmArtifactConfig.ComponentDescriptorLayer == nil { + return "", fmt.Errorf("%w for %s", ErrLayerNil, commonErrMsg(ocmi)) + } + + compDescLayerDigest := ocmArtifactConfig.ComponentDescriptorLayer.Digest + if string(compDescLayerDigest) == "" { + return "", + fmt.Errorf("%w for %s", ErrLayerDigestEmpty, commonErrMsg(ocmi)) + } + + return string(compDescLayerDigest), nil +} + +// deserialize decodes the component descriptor from its serialized form. +func deserialize(compdescBytes []byte, ocmi ocmidentity.ComponentId) (*compdesc.ComponentDescriptor, error) { + desc, err := compdesc.Decode(compdescBytes) + if err != nil { + return nil, fmt.Errorf("%w fetched from %s: %w", + ErrDecode, commonErrMsg(ocmi), err) + } + return desc, nil +} diff --git a/internal/service/componentdescriptor/service_test.go b/internal/service/componentdescriptor/service_test.go new file mode 100644 index 0000000000..cbd54a67b8 --- /dev/null +++ b/internal/service/componentdescriptor/service_test.go @@ -0,0 +1,303 @@ +package componentdescriptor_test + +import ( + "archive/tar" + "bytes" + "context" + "errors" + "testing" + + containerregistryv1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/static" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "ocm.software/ocm/api/ocm/compdesc" + compdescv2 "ocm.software/ocm/api/ocm/compdesc/versions/v2" + + "github.com/kyma-project/lifecycle-manager/internal/descriptor/types/ocmidentity" + "github.com/kyma-project/lifecycle-manager/internal/service/componentdescriptor" +) + +const ( + testComponentName = "kyma-project.io/test-component" + testComponentVersion = "0.1.2" + componentDescriptorMediaType = "application/vnd.ocm.software.component-descriptor.v2+yaml+tar" + testDigest = "sha256:4e51d8f80b88bdbd208e6e22314376a0d5212026bf3054f8ef79d43250e5182b" + invalidConfigNullLayer = `{"componentDescriptorLayer":null}` + baseComponentDescriptorConfig = `{"componentDescriptorLayer":{` + `"mediaType":"` + componentDescriptorMediaType + invalidConfigNoDigest = baseComponentDescriptorConfig + `","size":4608}}` + testComponentDescriptorConfig = baseComponentDescriptorConfig + `","digest":"` + testDigest + `","size":4608}}` +) + +func TestNewService(t *testing.T) { + t.Run("should return error when ociRepository is nil", func(t *testing.T) { + // when + svc, err := componentdescriptor.NewService(nil) + + // then + require.Nil(t, svc) + require.ErrorIs(t, err, componentdescriptor.ErrInvalidArg) + }) +} + +func TestGetComponentDescriptor(t *testing.T) { + compdesc.RegisterScheme(&compdescv2.DescriptorVersion{}) + + t.Run("should return component descriptor", func(t *testing.T) { + // given + cd := compdesc.New(testComponentName, testComponentVersion) + cdBytes, err := compdesc.Encode(cd) + require.NoError(t, err) + tarBytes := wrapAsTar(t, componentdescriptor.ComponentDescriptorFileName, cdBytes) + mockLayer := static.NewLayer(tarBytes, componentDescriptorMediaType) + + repo := mockOCIRepository{ + getConfigResult: []byte(testComponentDescriptorConfig), + pullLayerResult: mockLayer, + } + + svc, err := componentdescriptor.NewService(&repo) + require.NoError(t, err) + + ocmi, err := ocmidentity.NewComponentId(testComponentName, testComponentVersion) + require.NoError(t, err) + + // when + result, err := svc.GetComponentDescriptor(t.Context(), *ocmi) + + // then + require.NoError(t, err) + assert.Equal(t, testComponentName, result.GetName()) + assert.Equal(t, testComponentVersion, result.GetVersion()) + assert.Equal(t, testComponentVersion, repo.getConfigTag) + assert.Equal(t, testComponentName, repo.getConfigName) + assert.Equal(t, testComponentVersion, repo.pullLayerTag) + assert.Equal(t, testComponentName, repo.pullLayerName) + assert.Equal(t, testDigest, repo.pullLayerDigest) + }) + + t.Run("should fail when reading config object returns an error", func(t *testing.T) { + // given + repo := mockOCIRepository{ + getConfigError: errors.New("getConfigError"), // simulate error + } + + svc, err := componentdescriptor.NewService(&repo) + require.NoError(t, err) + ocmi, err := ocmidentity.NewComponentId(testComponentName, testComponentVersion) + require.NoError(t, err) + + // when + result, err := svc.GetComponentDescriptor(t.Context(), *ocmi) + // then + require.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), + "failed to get config file for ocm artifact with name=\""+ + testComponentName+ + "\" and version=\""+ + testComponentVersion) + assert.Contains(t, err.Error(), "getConfigError") + }) + + t.Run("should fail when config object is not valid json", func(t *testing.T) { + // given + repo := mockOCIRepository{ + getConfigResult: []byte("...invalid json..."), // simulate error + } + + svc, err := componentdescriptor.NewService(&repo) + require.NoError(t, err) + ocmi, err := ocmidentity.NewComponentId(testComponentName, testComponentVersion) + require.NoError(t, err) + + // when + result, err := svc.GetComponentDescriptor(t.Context(), *ocmi) + // then + require.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), + "failed to unmarshal config data into ComponentDescriptorConfig for ocm artifact with name=\""+ + testComponentName+ + "\" and version=\""+ + testComponentVersion) + assert.Contains(t, err.Error(), "invalid character") + }) + + t.Run("should fail when config object has null layer", func(t *testing.T) { + repo := mockOCIRepository{ + getConfigResult: []byte(invalidConfigNullLayer), + pullLayerResult: nil, + } + + svc, err := componentdescriptor.NewService(&repo) + require.NoError(t, err) + ocmi, err := ocmidentity.NewComponentId(testComponentName, testComponentVersion) + require.NoError(t, err) + + // when + result, err := svc.GetComponentDescriptor(t.Context(), *ocmi) + // then + require.Error(t, err) + assert.Nil(t, result) + require.ErrorIs(t, err, componentdescriptor.ErrLayerNil) + assert.Contains(t, err.Error(), "ComponentDescriptorLayer is nil in ComponentDescriptorConfig") + }) + + t.Run("should fail when config object has no digest", func(t *testing.T) { + repo := mockOCIRepository{ + getConfigResult: []byte(invalidConfigNoDigest), + pullLayerResult: nil, + } + + svc, err := componentdescriptor.NewService(&repo) + require.NoError(t, err) + ocmi, err := ocmidentity.NewComponentId(testComponentName, testComponentVersion) + require.NoError(t, err) + // when + result, err := svc.GetComponentDescriptor(t.Context(), *ocmi) + // then + assert.Nil(t, result) + require.Error(t, err) + require.ErrorIs(t, err, componentdescriptor.ErrLayerDigestEmpty) + assert.Contains(t, err.Error(), "ComponentDescriptorLayer.Digest is empty in ComponentDescriptorConfig") + }) + + t.Run("should fail when pulling layer returns an error", func(t *testing.T) { + // given + repo := mockOCIRepository{ + getConfigResult: []byte(testComponentDescriptorConfig), + pullLayerError: errors.New("pullLayerError"), // simulate error + } + + svc, err := componentdescriptor.NewService(&repo) + require.NoError(t, err) + ocmi, err := ocmidentity.NewComponentId(testComponentName, testComponentVersion) + require.NoError(t, err) + + // when + result, err := svc.GetComponentDescriptor(t.Context(), *ocmi) + // then + require.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), + "failed to pull layer for ocm artifact with name=\""+ + testComponentName+ + "\", version=\""+testComponentVersion+ + "\" and digest=\""+testDigest+"\"") + assert.Contains(t, err.Error(), "pullLayerError") + }) + + t.Run("should fail when tar archive doesn't contain expected file", func(t *testing.T) { + cd := compdesc.New(testComponentName, testComponentVersion) + compdesc.RegisterScheme(&compdescv2.DescriptorVersion{}) + cdBytes, err := compdesc.Encode(cd) + require.NoError(t, err) + tarBytes := wrapAsTar(t, "invalid-name", cdBytes) + mockLayer := static.NewLayer(tarBytes, componentDescriptorMediaType) + + repo := mockOCIRepository{ + getConfigResult: []byte(testComponentDescriptorConfig), + pullLayerResult: mockLayer, + } + + svc, err := componentdescriptor.NewService(&repo) + require.NoError(t, err) + + ocmi, err := ocmidentity.NewComponentId(testComponentName, testComponentVersion) + require.NoError(t, err) + + // when + result, err := svc.GetComponentDescriptor(t.Context(), *ocmi) + + // then + require.Error(t, err) + assert.Nil(t, result) + require.ErrorIs(t, err, componentdescriptor.ErrNotFoundInTar) + assert.Contains(t, err.Error(), "not found in TAR archive") + assert.Contains(t, err.Error(), "failed to extract data of file=\""+ + componentdescriptor.ComponentDescriptorFileName+"\"") + }) + + t.Run("should fail on component descriptor decoding error", func(t *testing.T) { + // given + cd := compdesc.New(testComponentName, testComponentVersion) + compdesc.RegisterScheme(&compdescv2.DescriptorVersion{}) + cdBytes, err := compdesc.Encode(cd) + require.NoError(t, err) + // introduce an error + cdBytes = bytes.ReplaceAll(cdBytes, []byte("[]"), []byte("!}")) + tarBytes := wrapAsTar(t, componentdescriptor.ComponentDescriptorFileName, cdBytes) + mockLayer := static.NewLayer(tarBytes, componentDescriptorMediaType) + + repo := mockOCIRepository{ + getConfigResult: []byte(testComponentDescriptorConfig), + pullLayerResult: mockLayer, + } + + svc, err := componentdescriptor.NewService(&repo) + require.NoError(t, err) + + ocmi, err := ocmidentity.NewComponentId(testComponentName, testComponentVersion) + require.NoError(t, err) + + // when + _, err = svc.GetComponentDescriptor(t.Context(), *ocmi) + + // then + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to decode component descriptor fetched from ocm artifact with name=\""+ + testComponentName+"\" and version=\""+testComponentVersion+"\"") + }) +} + +type mockOCIRepository struct { + getConfigName string + getConfigTag string + pullLayerName string + pullLayerTag string + pullLayerDigest string + getConfigResult []byte + getConfigError error + pullLayerResult containerregistryv1.Layer + pullLayerError error +} + +func (m *mockOCIRepository) GetConfigFile(ctx context.Context, name, tag string) ([]byte, error) { + m.getConfigName = name + m.getConfigTag = tag + if m.getConfigError != nil { + return nil, m.getConfigError + } + return m.getConfigResult, nil +} + +func (m *mockOCIRepository) PullLayer(ctx context.Context, name, tag, digest string) ( + containerregistryv1.Layer, error, +) { + m.pullLayerName = name + m.pullLayerTag = tag + m.pullLayerDigest = digest + if m.pullLayerError != nil { + return nil, m.pullLayerError + } + return m.pullLayerResult, nil +} + +func wrapAsTar(t *testing.T, fileName string, data []byte) []byte { + t.Helper() + var buf bytes.Buffer + twriter := tar.NewWriter(&buf) + err := twriter.WriteHeader(&tar.Header{ + Typeflag: tar.TypeReg, + Name: fileName, + Size: int64(len(data)), + Mode: 0o600, + }) + require.NoError(t, err) + _, err = twriter.Write(data) + require.NoError(t, err) + err = twriter.Close() + require.NoError(t, err) + return buf.Bytes() +} diff --git a/internal/service/componentdescriptor/untar.go b/internal/service/componentdescriptor/untar.go new file mode 100644 index 0000000000..a31dd35f12 --- /dev/null +++ b/internal/service/componentdescriptor/untar.go @@ -0,0 +1,68 @@ +package componentdescriptor + +import ( + "archive/tar" + "bytes" + "errors" + "fmt" + "io" +) + +const ( + MaxDescriptorSizeBytes = 100 * 1024 // 100KiB, our average is around 4KiB + TarReadChunkSize = 10 * 1024 // 10KiB, for our average size we'll read it in one go +) + +// tarExtractor is responsible for extracting named files from a tar archive. +type tarExtractor struct { + next func() (*tar.Header, error) // tar.Reader.Next() + copyN func(dst io.Writer, n int64) (int64, error) // modified io.CopyN() +} + +// unTar extracts the file with provided name and returns its content. +func (tarex *tarExtractor) unTar(name string) ([]byte, error) { + for { + hdr, err := tarex.next() + if errors.Is(err, io.EOF) { + break // end of archive + } + if err != nil { + return nil, err + } + + if hdr.Name == name { + var buf bytes.Buffer + maxSize := hdr.Size + if maxSize <= 0 { + maxSize = MaxDescriptorSizeBytes // sanity + } + if maxSize > MaxDescriptorSizeBytes { // DoS protection + return nil, fmt.Errorf("%s %w", name, ErrTarTooLarge) + } + for buf.Len() < int(maxSize) { // DoS protection: read in chunks + if _, err := tarex.copyN(&buf, TarReadChunkSize); err != nil { + if errors.Is(err, io.EOF) { + break + } + return nil, err + } + } + + return buf.Bytes(), nil + } + } + + return nil, fmt.Errorf("%s %w", name, ErrNotFoundInTar) +} + +func defaultTarExtractor(data []byte) *tarExtractor { + reader := tar.NewReader(bytes.NewReader(data)) + copyN := func(dst io.Writer, n int64) (int64, error) { + return io.CopyN(dst, reader, n) + } + + return &tarExtractor{ + next: reader.Next, + copyN: copyN, + } +} diff --git a/internal/service/componentdescriptor/untar_internal_test.go b/internal/service/componentdescriptor/untar_internal_test.go new file mode 100644 index 0000000000..35fc7ae016 --- /dev/null +++ b/internal/service/componentdescriptor/untar_internal_test.go @@ -0,0 +1,108 @@ +package componentdescriptor + +import ( + "archive/tar" + "bytes" + "errors" + "io" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUnTar(t *testing.T) { + smallInput := generateData(5 * 1024) + + t.Run("should return data from tar for small file", func(t *testing.T) { + tarred := asTar(smallInput, "testfile1") + res, err := defaultTarExtractor(tarred).unTar("testfile1") + require.NoError(t, err) + assert.Equal(t, smallInput, res) + }) + + t.Run("should return data from tar for large file", func(t *testing.T) { + input := generateData(50 * 1024) + tarred := asTar(input, "testfile2") + res, err := defaultTarExtractor(tarred).unTar("testfile2") + require.NoError(t, err) + assert.Equal(t, input, res) + }) + + t.Run("should return error when file not found", func(t *testing.T) { + input := generateData(9 * 1024) + tarred := asTar(input, "testfile3") + _, err := defaultTarExtractor(tarred).unTar("nonexisting") + require.ErrorIs(t, err, ErrNotFoundInTar) + }) + + t.Run("should return error when file too large", func(t *testing.T) { + input := generateData(150 * 1024) + tarred := asTar(input, "testfile4") + _, err := defaultTarExtractor(tarred).unTar("testfile4") + require.ErrorIs(t, err, ErrTarTooLarge) + }) + + t.Run("should return error when input is empty", func(t *testing.T) { + _, err := defaultTarExtractor([]byte{}).unTar("testfile") + require.ErrorIs(t, err, ErrNotFoundInTar) + }) + + t.Run("should return error when input is nil", func(t *testing.T) { + _, err := defaultTarExtractor(nil).unTar("testfile") + require.ErrorIs(t, err, ErrNotFoundInTar) + }) + + t.Run("should preserve original error when calling Next", func(t *testing.T) { + expectedErr := errors.New("problem calling Next") + subject := tarExtractor{ + next: func() (*tar.Header, error) { + return nil, expectedErr + }, + } + _, err := subject.unTar("testfile") + require.Error(t, err) + require.ErrorIs(t, err, expectedErr) + }) + + t.Run("should preserve original error when calling CopyN", func(t *testing.T) { + tarred := asTar(smallInput, "testfile4") + expectedErr := errors.New("problem calling CopyN") + subject := defaultTarExtractor(tarred) + subject.copyN = func(dst io.Writer, n int64) (int64, error) { + return 0, expectedErr + } + _, err := subject.unTar("testfile4") + require.Error(t, err) + require.ErrorIs(t, err, expectedErr) + }) +} + +func generateData(size int) []byte { + data := make([]byte, size) + for i := range size { + data[i] = byte(i%94 + 32) // ASCII 32 to 126 + } + return data +} + +func asTar(data []byte, filename string) []byte { + var buf bytes.Buffer + twriter := tar.NewWriter(&buf) + hdr := &tar.Header{ + Name: filename, + Mode: 0o600, + Size: int64(len(data)), + } + if err := twriter.WriteHeader(hdr); err != nil { + panic(err) + } + + if _, err := twriter.Write(data); err != nil { + panic(err) + } + if err := twriter.Close(); err != nil { + panic(err) + } + return buf.Bytes() +} diff --git a/internal/service/kyma/status/modules/generator/fromerror/generator.go b/internal/service/kyma/status/modules/generator/fromerror/generator.go index efdf858a85..270e06cab3 100644 --- a/internal/service/kyma/status/modules/generator/fromerror/generator.go +++ b/internal/service/kyma/status/modules/generator/fromerror/generator.go @@ -62,7 +62,8 @@ func errorIsMaintenanceWindowUnknown(err error) bool { } func errorIsForbiddenTemplateUpdate(err error) bool { - return errors.Is(err, templatelookup.ErrTemplateUpdateNotAllowed) + return errors.Is(err, templatelookup.ErrTemplateUpdateNotAllowed) || + errors.Is(err, templatelookup.ErrNoModuleReleaseMeta) } func errorIsTemplateNotFound(err error) bool { diff --git a/internal/service/kyma/status/modules/generator/fromerror/generator_test.go b/internal/service/kyma/status/modules/generator/fromerror/generator_test.go index f92ef52d6e..d6c2d174f6 100644 --- a/internal/service/kyma/status/modules/generator/fromerror/generator_test.go +++ b/internal/service/kyma/status/modules/generator/fromerror/generator_test.go @@ -118,6 +118,31 @@ func TestGenerateModuleStatusFromError_WhenCalledWithTemplateUpdateNotAllowedErr assert.NotEqual(t, someFQDN, result.FQDN) } +func TestGenerateModuleStatusFromError_WhenCalledWithErrNoModuleReleaseMeta_ReturnsDeepCopyAndStateWarning( + t *testing.T, +) { + someModuleName := "some-module" + someChannel := "some-channel" + someFQDN := "some-fqdn" + status := createStatus() + templateError := templatelookup.ErrNoModuleReleaseMeta + + result, err := fromerror.GenerateModuleStatusFromError(templateError, someModuleName, someChannel, someFQDN, status) + + assert.NotNil(t, result) + require.NoError(t, err) + + expectedStatus := status.DeepCopy() + expectedStatus.Message = templateError.Error() + expectedStatus.State = shared.StateWarning + assert.Equal(t, expectedStatus, result) + + // Passed module info is not used for new status, but the deep-copied object + assert.NotEqual(t, someModuleName, result.Name) + assert.NotEqual(t, someChannel, result.Channel) + assert.NotEqual(t, someFQDN, result.FQDN) +} + func TestGenerateModuleStatusFromError_WhenCalledWithNoTemplatesInListResultError_ReturnsNewStatusWithStateWarning( t *testing.T, ) { diff --git a/pkg/templatelookup/modulereleasemeta.go b/pkg/templatelookup/modulereleasemeta.go index c48329955a..4c281043c6 100644 --- a/pkg/templatelookup/modulereleasemeta.go +++ b/pkg/templatelookup/modulereleasemeta.go @@ -16,6 +16,8 @@ var ( ErrNoMandatoryFound = errors.New("no mandatory version found for module") ) +// GetModuleReleaseMeta finds the MRM by the name of module in the Kyma spec. +// The provide moduleName must match the name of the ModuleReleaseMeta K8s resource. func GetModuleReleaseMeta(ctx context.Context, clnt client.Reader, moduleName string, namespace string) (*v1beta2.ModuleReleaseMeta, error, diff --git a/pkg/templatelookup/moduletemplateinfo_test.go b/pkg/templatelookup/moduletemplateinfo_test.go new file mode 100644 index 0000000000..e9b616d34e --- /dev/null +++ b/pkg/templatelookup/moduletemplateinfo_test.go @@ -0,0 +1,34 @@ +package templatelookup_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/kyma-project/lifecycle-manager/api/v1beta2" + "github.com/kyma-project/lifecycle-manager/pkg/templatelookup" + "github.com/kyma-project/lifecycle-manager/pkg/testutils" +) + +func Test_GetOCMIdentity(t *testing.T) { + t.Run("When ComponentIdentity is nil, then an error is returned", func(t *testing.T) { + mtInfo := templatelookup.ModuleTemplateInfo{ + ModuleTemplate: &v1beta2.ModuleTemplate{}, + ComponentId: nil, + } + _, err := mtInfo.GetOCMIdentity() + require.Error(t, err) + require.ErrorIs(t, err, templatelookup.ErrNoIdentity) + assert.Contains(t, err.Error(), "component identity is nil") + }) + t.Run("When ComponentIdentity is not nil, then it is returned", func(t *testing.T) { + mtInfo := templatelookup.ModuleTemplateInfo{ + ComponentId: testutils.MustNewComponentId(testutils.FullOCMName("test-module"), "1.0.0"), + } + comp, err := mtInfo.GetOCMIdentity() + require.NoError(t, err) + assert.Equal(t, "kyma-project.io/module/test-module", comp.Name()) + assert.Equal(t, "1.0.0", comp.Version()) + }) +} diff --git a/pkg/templatelookup/moduletemplateinfolookup/by_module_release_meta_strategy.go b/pkg/templatelookup/moduletemplateinfolookup/by_module_release_meta_strategy.go index 5bb47f1739..66f5959519 100644 --- a/pkg/templatelookup/moduletemplateinfolookup/by_module_release_meta_strategy.go +++ b/pkg/templatelookup/moduletemplateinfolookup/by_module_release_meta_strategy.go @@ -6,6 +6,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "github.com/kyma-project/lifecycle-manager/api/v1beta2" + "github.com/kyma-project/lifecycle-manager/internal/descriptor/types/ocmidentity" "github.com/kyma-project/lifecycle-manager/pkg/templatelookup" ) @@ -34,12 +35,12 @@ func (s ByModuleReleaseMetaStrategy) Lookup(ctx context.Context, moduleTemplateInfo := templatelookup.ModuleTemplateInfo{} moduleTemplateInfo.DesiredChannel = getDesiredChannel(moduleInfo.Channel, kyma.Spec.Channel) - var desiredModuleVersion string + var resolvedModuleVersion string var err error if moduleReleaseMeta.Spec.Mandatory != nil { - desiredModuleVersion, err = templatelookup.GetMandatoryVersionForModule(moduleReleaseMeta) + resolvedModuleVersion, err = templatelookup.GetMandatoryVersionForModule(moduleReleaseMeta) } else { - desiredModuleVersion, err = templatelookup.GetChannelVersionForModule(moduleReleaseMeta, + resolvedModuleVersion, err = templatelookup.GetChannelVersionForModule(moduleReleaseMeta, moduleTemplateInfo.DesiredChannel) } if err != nil { @@ -47,10 +48,18 @@ func (s ByModuleReleaseMetaStrategy) Lookup(ctx context.Context, return moduleTemplateInfo } + if ocmi, err := ocmidentity.NewComponentId( + moduleReleaseMeta.Spec.OcmComponentName, resolvedModuleVersion); err != nil { + moduleTemplateInfo.Err = err + return moduleTemplateInfo + } else { + moduleTemplateInfo.ComponentId = ocmi + } + template, err := getTemplateByVersion(ctx, s.client, moduleInfo.Name, - desiredModuleVersion, + resolvedModuleVersion, kyma.Namespace) if err != nil { moduleTemplateInfo.Err = err diff --git a/pkg/templatelookup/moduletemplateinfolookup/by_module_release_meta_strategy_test.go b/pkg/templatelookup/moduletemplateinfolookup/by_module_release_meta_strategy_test.go index 58fee1137a..d4a438f8d8 100644 --- a/pkg/templatelookup/moduletemplateinfolookup/by_module_release_meta_strategy_test.go +++ b/pkg/templatelookup/moduletemplateinfolookup/by_module_release_meta_strategy_test.go @@ -3,6 +3,7 @@ package moduletemplateinfolookup_test import ( "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" machineryruntime "k8s.io/apimachinery/pkg/runtime" machineryutilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -12,6 +13,7 @@ import ( "github.com/kyma-project/lifecycle-manager/api" "github.com/kyma-project/lifecycle-manager/api/v1beta2" "github.com/kyma-project/lifecycle-manager/pkg/templatelookup/moduletemplateinfolookup" + "github.com/kyma-project/lifecycle-manager/pkg/testutils" "github.com/kyma-project/lifecycle-manager/pkg/testutils/builder" ) @@ -41,6 +43,7 @@ func Test_ByModuleReleaseMeta_Strategy_Lookup_ReturnsModuleTemplateInfo(t *testi moduleReleaseMeta := builder.NewModuleReleaseMetaBuilder(). WithModuleName("test-module"). WithName("test-module"). + WithOcmComponentName(testutils.FullOCMName("test-module")). WithModuleChannelAndVersions([]v1beta2.ChannelVersionAssignment{ { Channel: "regular", @@ -64,10 +67,11 @@ func Test_ByModuleReleaseMeta_Strategy_Lookup_ReturnsModuleTemplateInfo(t *testi moduleTemplateInfo := byMRMStrategy.Lookup(t.Context(), moduleInfo, kyma, moduleReleaseMeta) require.NotNil(t, moduleTemplateInfo) - require.Equal(t, moduleTemplate.Name, moduleTemplateInfo.Name) - require.Equal(t, moduleTemplate.Spec.ModuleName, moduleTemplateInfo.Spec.ModuleName) - require.Equal(t, moduleTemplate.Spec.Version, moduleTemplateInfo.Spec.Version) - require.Equal(t, moduleTemplate.Spec.Channel, moduleTemplateInfo.Spec.Channel) + require.NotNil(t, moduleTemplateInfo.ModuleTemplate) + assert.Equal(t, moduleTemplate.Name, moduleTemplateInfo.Name) + assert.Equal(t, moduleTemplate.Spec.ModuleName, moduleTemplateInfo.Spec.ModuleName) + assert.Equal(t, moduleTemplate.Spec.Version, moduleTemplateInfo.Spec.Version) + assert.Equal(t, moduleTemplate.Spec.Channel, moduleTemplateInfo.Spec.Channel) } func Test_ByModuleReleaseMeta_Strategy_Lookup_WhenGetChannelVersionForModuleReturnsError(t *testing.T) { @@ -76,6 +80,7 @@ func Test_ByModuleReleaseMeta_Strategy_Lookup_WhenGetChannelVersionForModuleRetu moduleReleaseMeta := builder.NewModuleReleaseMetaBuilder(). WithModuleName("test-module"). WithName("test-module"). + WithOcmComponentName(testutils.FullOCMName("test-module")). WithModuleChannelAndVersions([]v1beta2.ChannelVersionAssignment{ { Channel: "regular", @@ -137,6 +142,7 @@ func Test_ByModuleReleaseMeta_Strategy_Lookup_WhenMandatoryModuleActivated_Retur moduleReleaseMeta := builder.NewModuleReleaseMetaBuilder(). WithModuleName("test-module"). WithName("test-module"). + WithOcmComponentName(testutils.FullOCMName("test-module")). WithMandatory("1.0.0"). Build() moduleTemplate := builder.NewModuleTemplateBuilder(). @@ -156,9 +162,10 @@ func Test_ByModuleReleaseMeta_Strategy_Lookup_WhenMandatoryModuleActivated_Retur moduleTemplateInfo := byMRMStrategy.Lookup(t.Context(), moduleInfo, kyma, moduleReleaseMeta) require.NotNil(t, moduleTemplateInfo) - require.Equal(t, moduleTemplate.Name, moduleTemplateInfo.Name) - require.Equal(t, moduleTemplate.Spec.ModuleName, moduleTemplateInfo.Spec.ModuleName) - require.Equal(t, moduleTemplate.Spec.Version, moduleTemplateInfo.Spec.Version) + require.NotNil(t, moduleTemplateInfo.ModuleTemplate) + assert.Equal(t, moduleTemplate.Name, moduleTemplateInfo.Name) + assert.Equal(t, moduleTemplate.Spec.ModuleName, moduleTemplateInfo.Spec.ModuleName) + assert.Equal(t, moduleTemplate.Spec.Version, moduleTemplateInfo.Spec.Version) } func fakeClient(mts *v1beta2.ModuleTemplateList) client.Client { diff --git a/pkg/templatelookup/moduletemplateinfolookup/by_version_strategy.go b/pkg/templatelookup/moduletemplateinfolookup/by_version_strategy.go index aede4a0300..cdab3ad64f 100644 --- a/pkg/templatelookup/moduletemplateinfolookup/by_version_strategy.go +++ b/pkg/templatelookup/moduletemplateinfolookup/by_version_strategy.go @@ -29,11 +29,7 @@ func (ByVersionStrategy) IsResponsible( return false } - if !moduleInfo.IsInstalledByVersion() { - return false - } - - return true + return moduleInfo.IsInstalledByVersion() } func (s ByVersionStrategy) Lookup(ctx context.Context, diff --git a/pkg/templatelookup/regular.go b/pkg/templatelookup/regular.go index 7f7b7f9344..9a1c469b3d 100644 --- a/pkg/templatelookup/regular.go +++ b/pkg/templatelookup/regular.go @@ -11,19 +11,34 @@ 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/descriptor/types/ocmidentity" "github.com/kyma-project/lifecycle-manager/pkg/templatelookup/common" ) var ( ErrTemplateNotAllowed = errors.New("module template not allowed") ErrTemplateUpdateNotAllowed = errors.New("module template update not allowed") + ErrNoModuleReleaseMeta = errors.New("no ModuleReleaseMeta found") + ErrNoIdentity = errors.New("component identity is nil") ) type ModuleTemplateInfo struct { *v1beta2.ModuleTemplate Err error - DesiredChannel string + DesiredChannel string // This is the channel that was requested by the user + // using Kyma 'spec.channel' or configured module channel. + + ComponentId *ocmidentity.ComponentId // Identifies the OCM Component that is + // represented by this ModuleTemplateInfo. +} + +// Implements provider.OCMIProvider interface. +func (m ModuleTemplateInfo) GetOCMIdentity() (*ocmidentity.ComponentId, error) { + if m.ComponentId == nil { + return nil, fmt.Errorf("%w for module template %s", ErrNoIdentity, m.Name) + } + return m.ComponentId, nil } type ModuleTemplateInfoLookupStrategy interface { @@ -72,6 +87,13 @@ func (t *TemplateLookup) GetRegularTemplates(ctx context.Context, kyma *v1beta2. continue } + if moduleReleaseMeta == nil { + msg := fmt.Sprintf(" for module %q in namespace %q", + moduleInfo.Name, kyma.Namespace) + templates[moduleInfo.Name] = &ModuleTemplateInfo{Err: fmt.Errorf("%w %s", ErrNoModuleReleaseMeta, msg)} + continue + } + templateInfo := t.moduleTemplateInfoLookupStrategy.Lookup(ctx, &moduleInfo, kyma, @@ -82,7 +104,15 @@ func (t *TemplateLookup) GetRegularTemplates(ctx context.Context, kyma *v1beta2. templates[moduleInfo.Name] = &templateInfo continue } - if err := t.descriptorProvider.Add(templateInfo.ModuleTemplate); err != nil { + + ocmi, err := ocmidentity.NewComponentId(moduleReleaseMeta.Spec.OcmComponentName, templateInfo.Spec.Version) + if err != nil { + templateInfo.Err = fmt.Errorf("failed to create OCM Component Identity: %w", err) + templates[moduleInfo.Name] = &templateInfo + continue + } + + if err := t.descriptorProvider.Add(*ocmi); err != nil { templateInfo.Err = fmt.Errorf("failed to get descriptor: %w", err) templates[moduleInfo.Name] = &templateInfo continue @@ -90,13 +120,7 @@ func (t *TemplateLookup) GetRegularTemplates(ctx context.Context, kyma *v1beta2. for i := range kyma.Status.Modules { moduleStatus := &kyma.Status.Modules[i] if moduleMatch(moduleStatus, moduleInfo.Name) { - descriptor, err := t.descriptorProvider.GetDescriptor(templateInfo.ModuleTemplate) - if err != nil { - msg := "could not handle channel skew as descriptor from template cannot be fetched" - templateInfo.Err = fmt.Errorf("%w: %s", ErrTemplateUpdateNotAllowed, msg) - continue - } - markInvalidSkewUpdate(ctx, &templateInfo, moduleStatus, descriptor.Version) + markInvalidSkewUpdate(ctx, &templateInfo, moduleStatus, ocmi.Version()) } } templates[moduleInfo.Name] = &templateInfo diff --git a/pkg/templatelookup/regular_test.go b/pkg/templatelookup/regular_test.go index a13552d8dd..b03f9e8e47 100644 --- a/pkg/templatelookup/regular_test.go +++ b/pkg/templatelookup/regular_test.go @@ -11,14 +11,12 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime/schema" "ocm.software/ocm/api/ocm/compdesc" - ocmmetav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" - compdescv2 "ocm.software/ocm/api/ocm/compdesc/versions/v2" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/kyma-project/lifecycle-manager/api/shared" "github.com/kyma-project/lifecycle-manager/api/v1beta2" "github.com/kyma-project/lifecycle-manager/internal/descriptor/provider" - "github.com/kyma-project/lifecycle-manager/internal/descriptor/types" + "github.com/kyma-project/lifecycle-manager/internal/service/componentdescriptor" "github.com/kyma-project/lifecycle-manager/pkg/templatelookup" "github.com/kyma-project/lifecycle-manager/pkg/templatelookup/common" "github.com/kyma-project/lifecycle-manager/pkg/templatelookup/moduletemplateinfolookup" @@ -131,8 +129,8 @@ func TestValidateTemplateMode_ForOldModuleTemplates(t *testing.T) { } for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { - if got := templatelookup.ValidateTemplateMode(testCase.template, testCase.kyma); !errors.Is(got.Err, - testCase.wantErr) { + if got := templatelookup.ValidateTemplateMode(testCase.template, testCase.kyma); !errors.Is( + got.Err, testCase.wantErr) { t.Errorf("ValidateTemplateMode() = %v, want %v", got, testCase.wantErr) } }) @@ -176,7 +174,7 @@ func Test_GetRegularTemplates_WhenInvalidModuleProvided(t *testing.T) { t.Run(tt.name, func(t *testing.T) { lookup := templatelookup.NewTemplateLookup( nil, - provider.NewCachedDescriptorProvider(), + nil, // not used in tests moduletemplateinfolookup.NewModuleTemplateInfoLookupStrategies( []moduletemplateinfolookup.ModuleTemplateInfoLookupStrategy{ moduletemplateinfolookup.NewByVersionStrategy(nil), @@ -202,6 +200,13 @@ func Test_GetRegularTemplates_WhenInvalidModuleProvided(t *testing.T) { func TestTemplateLookup_GetRegularTemplates_WhenSwitchModuleChannel(t *testing.T) { testModule := testutils.NewTestModule("module1", "new_channel") + fakeService := &componentdescriptor.FakeService{} + descriptorProvider := provider.NewCachedDescriptorProvider(fakeService) // cache backed up by a fake service + err := registerEmptyComponentDescriptor(fakeService, testutils.FullOCMName(testModule.Name), version1) + require.NoError(t, err) + err = registerEmptyComponentDescriptor(fakeService, testutils.FullOCMName(testModule.Name), version2) + require.NoError(t, err) + tests := []struct { name string kyma *v1beta2.Kyma @@ -209,58 +214,19 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchModuleChannel(t *testing.T availableModuleReleaseMeta v1beta2.ModuleReleaseMetaList want templatelookup.ModuleTemplatesByModuleName }{ - { - name: "When upgrade version during channel switch, " + - "then result contains no error, without ModuleReleaseMeta", - kyma: builder.NewKymaBuilder(). - WithEnabledModule(testModule). - WithModuleStatus(v1beta2.ModuleStatus{ - Name: testModule.Name, - Channel: v1beta2.DefaultChannel, - Version: version1, - Template: &v1beta2.TrackingObject{ - PartialMeta: v1beta2.PartialMeta{ - Generation: 1, - }, - }, - }).Build(), - availableModuleTemplate: generateModuleTemplateListWithModule(testModule.Name, testModule.Channel, - version2), - availableModuleReleaseMeta: v1beta2.ModuleReleaseMetaList{}, - want: templatelookup.ModuleTemplatesByModuleName{ - testModule.Name: &templatelookup.ModuleTemplateInfo{ - DesiredChannel: testModule.Channel, - Err: nil, - }, - }, - }, - { - name: "When downgrade version during channel switch, Then result contains error, without ModuleReleaseMeta", - kyma: builder.NewKymaBuilder(). - WithEnabledModule(testModule). - WithModuleStatus(v1beta2.ModuleStatus{ - Name: testModule.Name, - Channel: v1beta2.DefaultChannel, - Version: version2, - Template: &v1beta2.TrackingObject{ - PartialMeta: v1beta2.PartialMeta{ - Generation: 1, - }, - }, - }).Build(), - availableModuleTemplate: generateModuleTemplateListWithModule(testModule.Name, testModule.Channel, - version1), - availableModuleReleaseMeta: v1beta2.ModuleReleaseMetaList{}, - - want: templatelookup.ModuleTemplatesByModuleName{ - testModule.Name: &templatelookup.ModuleTemplateInfo{ - DesiredChannel: testModule.Channel, - Err: templatelookup.ErrTemplateUpdateNotAllowed, - }, + /* + // modules without ModuleReleaseMeta are not supported anymore! + { + name: "When upgrade version during channel switch, " + + "then result contains no error, without ModuleReleaseMeta", + },{ + name: "When downgrade version during channel switch, " + + "then result contains error, without ModuleReleaseMeta", }, - }, + */ { - name: "When upgrade version during channel switch, Then result contains no error, with ModuleReleaseMeta", + name: "When upgrade version during channel switch, " + + "then result contains no error, with ModuleReleaseMeta", kyma: builder.NewKymaBuilder(). WithEnabledModule(testModule). WithModuleStatus(v1beta2.ModuleStatus{ @@ -288,7 +254,8 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchModuleChannel(t *testing.T }, }, { - name: "When downgrade version during channel switch, Then result contains error, with ModuleReleaseMeta", + name: "When downgrade version during channel switch, " + + "then result contains error, with ModuleReleaseMeta", kyma: builder.NewKymaBuilder(). WithEnabledModule(testModule). WithModuleStatus(v1beta2.ModuleStatus{ @@ -317,10 +284,13 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchModuleChannel(t *testing.T for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { - reader := NewFakeModuleTemplateReader(testCase.availableModuleTemplate, testCase.availableModuleReleaseMeta) + reader := NewFakeModuleTemplateReader( + testCase.availableModuleTemplate, + testCase.availableModuleReleaseMeta, + ) lookup := templatelookup.NewTemplateLookup( reader, - provider.NewCachedDescriptorProvider(), + descriptorProvider, moduletemplateinfolookup.NewModuleTemplateInfoLookupStrategies( []moduletemplateinfolookup.ModuleTemplateInfoLookupStrategy{ moduletemplateinfolookup.NewByVersionStrategy(reader), @@ -343,6 +313,7 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchModuleChannel(t *testing.T } func TestTemplateLookup_GetRegularTemplates_WhenSwitchBetweenModuleVersions(t *testing.T) { + t.Skip("This test verifies install-by-version which is not supported yet in the logic based on ModuleReleaseMeta") moduleToInstall := moduleToInstallByVersion("module1", version2) availableModuleTemplates := (&ModuleTemplateListBuilder{}). @@ -354,8 +325,25 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchBetweenModuleVersions(t *t Add(moduleToInstall.Name, string(shared.NoneChannel), version3). Build() - availableModuleReleaseMetas := v1beta2.ModuleReleaseMetaList{} + availableModuleReleaseMetas := generateModuleReleaseMetaList(moduleToInstall.Name, + []v1beta2.ChannelVersionAssignment{ + {Channel: "regular", Version: version1}, + {Channel: "fast", Version: version2}, + {Channel: "experimental", Version: version3}, + {Channel: string(shared.NoneChannel), Version: version2}, + }) + fakeService := &componentdescriptor.FakeService{} + descriptorProvider := provider.NewCachedDescriptorProvider(fakeService) // cache backed up by a fake service + err := registerEmptyComponentDescriptor(fakeService, "kyma-project.io/module"+ + "/"+moduleToInstall.Name, version1) + require.NoError(t, err) + err = registerEmptyComponentDescriptor(fakeService, "kyma-project.io/module"+ + "/"+moduleToInstall.Name, version2) + require.NoError(t, err) + err = registerEmptyComponentDescriptor(fakeService, "kyma-project.io/module"+ + "/"+moduleToInstall.Name, version2) + require.NoError(t, err) tests := getRegularTemplatesTestCases{ { name: "When upgrade version, then result contains no error", @@ -393,10 +381,11 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchBetweenModuleVersions(t *t } executeGetRegularTemplatesTestCases(t, tests, availableModuleTemplates, availableModuleReleaseMetas, - moduleToInstall) + moduleToInstall, descriptorProvider) } func TestTemplateLookup_GetRegularTemplates_WhenSwitchFromChannelToVersion(t *testing.T) { + t.Skip("This test verifies install-by-version which is not supported yet in the logic based on ModuleReleaseMeta") moduleToInstall := moduleToInstallByVersion("module1", version2) availableModuleTemplates := (&ModuleTemplateListBuilder{}). Add(moduleToInstall.Name, "regular", version1). @@ -463,7 +452,7 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchFromChannelToVersion(t *te } executeGetRegularTemplatesTestCases(t, tests, availableModuleTemplates, availableModuleReleaseMetas, - moduleToInstall) + moduleToInstall, nil) } func TestTemplateLookup_GetRegularTemplates_WhenSwitchFromVersionToChannel(t *testing.T) { @@ -477,7 +466,17 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchFromVersionToChannel(t *te Add(moduleToInstall.Name, string(shared.NoneChannel), version3). Build() - availableModuleReleaseMetas := v1beta2.ModuleReleaseMetaList{} + availableModuleReleaseMetas := generateModuleReleaseMetaList( + moduleToInstall.Name, + []v1beta2.ChannelVersionAssignment{ + {Channel: "new_channel", Version: version2}, + }, + ) + + fakeService := &componentdescriptor.FakeService{} + descriptorProvider := provider.NewCachedDescriptorProvider(fakeService) + err := registerEmptyComponentDescriptor(fakeService, testutils.FullOCMName(moduleToInstall.Name), version2) + require.NoError(t, err) tests := getRegularTemplatesTestCases{ { @@ -533,10 +532,11 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchFromVersionToChannel(t *te } executeGetRegularTemplatesTestCases(t, tests, availableModuleTemplates, availableModuleReleaseMetas, - moduleToInstall) + moduleToInstall, descriptorProvider) } func TestNewTemplateLookup_GetRegularTemplates_WhenModuleTemplateContainsInvalidDescriptor(t *testing.T) { + t.Skip("This test is not relevant anymore as we no longer read ComponentDescriptor from ModuleTemplate") testModule := testutils.NewTestModule("module1", v1beta2.DefaultChannel) tests := []struct { name string @@ -544,40 +544,44 @@ func TestNewTemplateLookup_GetRegularTemplates_WhenModuleTemplateContainsInvalid mrmExists bool want templatelookup.ModuleTemplatesByModuleName }{ - { - name: "When module enabled in Spec, then return ModuleTemplatesByModuleName with error, " + - "without ModuleReleaseMeta", - kyma: builder.NewKymaBuilder(). - WithEnabledModule(testModule).Build(), - mrmExists: false, - want: templatelookup.ModuleTemplatesByModuleName{ - testModule.Name: &templatelookup.ModuleTemplateInfo{ - DesiredChannel: testModule.Channel, - Err: provider.ErrDecode, + /* + // modules without ModuleReleaseMeta are not supported anymore! + { + name: "When module enabled in Spec, then return ModuleTemplatesByModuleName with error, " + + "without ModuleReleaseMeta", + kyma: builder.NewKymaBuilder(). + WithEnabledModule(testModule).Build(), + mrmExists: false, + want: templatelookup.ModuleTemplatesByModuleName{ + testModule.Name: &templatelookup.ModuleTemplateInfo{ + DesiredChannel: testModule.Channel, + Err: provider.ErrDecode, + }, }, }, - }, - { - name: "When module exits in ModuleStatus only, then return ModuleTemplatesByModuleName with error," + - "without ModuleReleaseMeta", - kyma: builder.NewKymaBuilder(). - WithModuleStatus(v1beta2.ModuleStatus{ - Name: testModule.Name, - Channel: testModule.Channel, - Template: &v1beta2.TrackingObject{ - PartialMeta: v1beta2.PartialMeta{ - Generation: 1, + // modules without ModuleReleaseMeta are not supported anymore! + { + name: "When module exits in ModuleStatus only, then return ModuleTemplatesByModuleName with error," + + "without ModuleReleaseMeta", + kyma: builder.NewKymaBuilder(). + WithModuleStatus(v1beta2.ModuleStatus{ + Name: testModule.Name, + Channel: testModule.Channel, + Template: &v1beta2.TrackingObject{ + PartialMeta: v1beta2.PartialMeta{ + Generation: 1, + }, }, + }).Build(), + mrmExists: false, + want: templatelookup.ModuleTemplatesByModuleName{ + testModule.Name: &templatelookup.ModuleTemplateInfo{ + DesiredChannel: testModule.Channel, + Err: provider.ErrDecode, }, - }).Build(), - mrmExists: false, - want: templatelookup.ModuleTemplatesByModuleName{ - testModule.Name: &templatelookup.ModuleTemplateInfo{ - DesiredChannel: testModule.Channel, - Err: provider.ErrDecode, }, }, - }, + */ { name: "When module enabled in Spec, then return ModuleTemplatesByModuleName with error," + "with ModuleReleaseMeta", @@ -618,17 +622,19 @@ func TestNewTemplateLookup_GetRegularTemplates_WhenModuleTemplateContainsInvalid givenTemplateList := &v1beta2.ModuleTemplateList{} moduleReleaseMetas := v1beta2.ModuleReleaseMetaList{} for _, module := range templatelookup.FetchModuleInfo(testCase.kyma) { - givenTemplateList.Items = append(givenTemplateList.Items, *builder.NewModuleTemplateBuilder(). - WithName(fmt.Sprintf("%s-%s", module.Name, testModule.Version)). - WithModuleName(module.Name). - WithChannel(module.Channel). - WithDescriptor(nil). - WithRawDescriptor([]byte("{invalid_json}")).Build()) + givenTemplateList.Items = append(givenTemplateList.Items, + *builder.NewModuleTemplateBuilder(). + WithName(fmt.Sprintf("%s-%s", module.Name, testModule.Version)). + WithModuleName(module.Name). + WithChannel(module.Channel). + WithDescriptor(nil). + WithRawDescriptor([]byte("{invalid_json}")).Build()) if testCase.mrmExists { moduleReleaseMetas.Items = append(moduleReleaseMetas.Items, *builder.NewModuleReleaseMetaBuilder(). WithModuleName(module.Name). + WithOcmComponentName(testutils.FullOCMName(module.Name)). WithModuleChannelAndVersions([]v1beta2.ChannelVersionAssignment{ {Channel: module.Channel, Version: testModule.Version}, }).Build()) @@ -636,9 +642,8 @@ func TestNewTemplateLookup_GetRegularTemplates_WhenModuleTemplateContainsInvalid } reader := NewFakeModuleTemplateReader(*givenTemplateList, moduleReleaseMetas) - lookup := templatelookup.NewTemplateLookup( - reader, - provider.NewCachedDescriptorProvider(), + lookup := templatelookup.NewTemplateLookup(reader, + nil, // not used in tests moduletemplateinfolookup.NewModuleTemplateInfoLookupStrategies( []moduletemplateinfolookup.ModuleTemplateInfoLookupStrategy{ moduletemplateinfolookup.NewByVersionStrategy(reader), @@ -661,8 +666,14 @@ func TestNewTemplateLookup_GetRegularTemplates_WhenModuleTemplateContainsInvalid } func TestTemplateLookup_GetRegularTemplates_WhenModuleTemplateNotFound(t *testing.T) { + t.Skip("This test is not using ModuleReleaseMeta." + + " It must be adjusted when the old logic, based on ModuleTemplates" + + " is removed") testModule := testutils.NewTestModule("module1", v1beta2.DefaultChannel) + fakeService := &componentdescriptor.FakeService{} + descriptorProvider := provider.NewCachedDescriptorProvider(fakeService) // cache backed up by a fake service + tests := []struct { name string kyma *v1beta2.Kyma @@ -711,7 +722,7 @@ func TestTemplateLookup_GetRegularTemplates_WhenModuleTemplateNotFound(t *testin v1beta2.ModuleReleaseMetaList{}) lookup := templatelookup.NewTemplateLookup( reader, - provider.NewCachedDescriptorProvider(), + descriptorProvider, moduletemplateinfolookup.NewModuleTemplateInfoLookupStrategies( []moduletemplateinfolookup.ModuleTemplateInfoLookupStrategy{ moduletemplateinfolookup.NewByVersionStrategy(reader), @@ -736,6 +747,12 @@ func TestTemplateLookup_GetRegularTemplates_WhenModuleTemplateNotFound(t *testin func TestTemplateLookup_GetRegularTemplates_WhenModuleTemplateExists(t *testing.T) { testModule := testutils.NewTestModule("module1", v1beta2.DefaultChannel) + const moduleVersion = "1.0.0" + + fakeService := &componentdescriptor.FakeService{} + descriptorProvider := provider.NewCachedDescriptorProvider(fakeService) + err := registerEmptyComponentDescriptor(fakeService, testutils.FullOCMName(testModule.Name), moduleVersion) + require.NoError(t, err) tests := []struct { name string @@ -743,22 +760,25 @@ func TestTemplateLookup_GetRegularTemplates_WhenModuleTemplateExists(t *testing. mrmExist bool want templatelookup.ModuleTemplatesByModuleName }{ - { - name: "When module enabled in Spec, then return expected moduleTemplateInfo, without ModuleReleaseMeta", - kyma: builder.NewKymaBuilder(). - WithEnabledModule(testModule).Build(), - mrmExist: false, - want: templatelookup.ModuleTemplatesByModuleName{ - testModule.Name: &templatelookup.ModuleTemplateInfo{ - DesiredChannel: testModule.Channel, - Err: nil, - ModuleTemplate: builder.NewModuleTemplateBuilder(). - WithModuleName(testModule.Name). - WithChannel(testModule.Channel). - Build(), + /* + //Removed: Modules without ModuleReleaseMeta are not supported anymore! + { + name: "When module enabled in Spec, then return expected moduleTemplateInfo, without ModuleReleaseMeta", + kyma: builder.NewKymaBuilder(). + WithEnabledModule(testModule).Build(), + mrmExist: false, + want: templatelookup.ModuleTemplatesByModuleName{ + testModule.Name: &templatelookup.ModuleTemplateInfo{ + DesiredChannel: testModule.Channel, + Err: nil, + ModuleTemplate: builder.NewModuleTemplateBuilder(). + WithModuleName(testModule.Name). + WithChannel(testModule.Channel). + Build(), + }, }, }, - }, + */ { name: "When module enabled in Spec, then return expected moduleTemplateInfo, with ModuleReleaseMeta", kyma: builder.NewKymaBuilder(). @@ -770,38 +790,42 @@ func TestTemplateLookup_GetRegularTemplates_WhenModuleTemplateExists(t *testing. Err: nil, ModuleTemplate: builder.NewModuleTemplateBuilder(). WithModuleName(testModule.Name). + WithVersion(moduleVersion). WithChannel(""). Build(), }, }, }, - { - name: "When module exits in ModuleStatus only, " + - "then return expected moduleTemplateInfo, without ModuleReleaseMeta", - kyma: builder.NewKymaBuilder(). - WithEnabledModule(testModule). - WithModuleStatus(v1beta2.ModuleStatus{ - Name: testModule.Name, - Channel: testModule.Channel, - Template: &v1beta2.TrackingObject{ - PartialMeta: v1beta2.PartialMeta{ - Generation: 1, + /* + // Modules without ModuleReleaseMeta are not supported anymore! + { + name: "When module exits in ModuleStatus only, " + + "then return expected moduleTemplateInfo, without ModuleReleaseMeta", + kyma: builder.NewKymaBuilder(). + WithEnabledModule(testModule). + WithModuleStatus(v1beta2.ModuleStatus{ + Name: testModule.Name, + Channel: testModule.Channel, + Template: &v1beta2.TrackingObject{ + PartialMeta: v1beta2.PartialMeta{ + Generation: 1, + }, }, + Version: "1.0.0", + }).Build(), + mrmExist: false, + want: templatelookup.ModuleTemplatesByModuleName{ + testModule.Name: &templatelookup.ModuleTemplateInfo{ + DesiredChannel: testModule.Channel, + Err: nil, + ModuleTemplate: builder.NewModuleTemplateBuilder(). + WithModuleName(testModule.Name). + WithChannel(testModule.Channel). + Build(), }, - Version: "1.0.0", - }).Build(), - mrmExist: false, - want: templatelookup.ModuleTemplatesByModuleName{ - testModule.Name: &templatelookup.ModuleTemplateInfo{ - DesiredChannel: testModule.Channel, - Err: nil, - ModuleTemplate: builder.NewModuleTemplateBuilder(). - WithModuleName(testModule.Name). - WithChannel(testModule.Channel). - Build(), }, }, - }, + */ { name: "When module exits in ModuleStatus only, " + "then return expected moduleTemplateInfo, with ModuleReleaseMeta", @@ -841,10 +865,11 @@ func TestTemplateLookup_GetRegularTemplates_WhenModuleTemplateExists(t *testing. WithName(fmt.Sprintf("%s-%s", module.Name, moduleTemplateVersion)). WithModuleName(module.Name). WithVersion(moduleTemplateVersion). - WithOCM(compdescv2.SchemaVersion).Build()) + Build()) moduleReleaseMetas.Items = append(moduleReleaseMetas.Items, *builder.NewModuleReleaseMetaBuilder(). WithModuleName(module.Name). + WithOcmComponentName(testutils.FullOCMName(module.Name)). WithModuleChannelAndVersions([]v1beta2.ChannelVersionAssignment{ {Channel: module.Channel, Version: moduleTemplateVersion}, }).Build()) @@ -854,14 +879,14 @@ func TestTemplateLookup_GetRegularTemplates_WhenModuleTemplateExists(t *testing. WithModuleName(module.Name). WithVersion(moduleTemplateVersion). WithChannel(module.Channel). - WithOCM(compdescv2.SchemaVersion).Build()) + Build()) } } reader := NewFakeModuleTemplateReader(*givenTemplateList, moduleReleaseMetas) lookup := templatelookup.NewTemplateLookup( reader, - provider.NewCachedDescriptorProvider(), + descriptorProvider, moduletemplateinfolookup.NewModuleTemplateInfoLookupStrategies( []moduletemplateinfolookup.ModuleTemplateInfoLookupStrategy{ moduletemplateinfolookup.NewByVersionStrategy(reader), @@ -945,6 +970,7 @@ func executeGetRegularTemplatesTestCases(t *testing.T, availableModuleTemplates v1beta2.ModuleTemplateList, availableModuleReleaseMetas v1beta2.ModuleReleaseMetaList, moduleToInstall v1beta2.Module, + descriptorProvider *provider.CachedDescriptorProvider, ) { t.Helper() for _, testCase := range testCases { @@ -952,7 +978,7 @@ func executeGetRegularTemplatesTestCases(t *testing.T, reader := NewFakeModuleTemplateReader(availableModuleTemplates, availableModuleReleaseMetas) lookup := templatelookup.NewTemplateLookup( reader, - provider.NewCachedDescriptorProvider(), + descriptorProvider, moduletemplateinfolookup.NewModuleTemplateInfoLookupStrategies( []moduletemplateinfolookup.ModuleTemplateInfoLookupStrategy{ moduletemplateinfolookup.NewByVersionStrategy(reader), @@ -963,13 +989,15 @@ func executeGetRegularTemplatesTestCases(t *testing.T, ) got := lookup.GetRegularTemplates(t.Context(), testCase.kyma) assert.Len(t, got, 1) - for key, module := range got { + for key, moduleTemplateInfo := range got { + require.NotNil(t, moduleTemplateInfo) + require.NotNil(t, moduleTemplateInfo.ModuleTemplate) assert.Equal(t, key, moduleToInstall.Name) if testCase.wantErrContains != "" { - assert.Contains(t, module.Err.Error(), testCase.wantErrContains) + assert.Contains(t, moduleTemplateInfo.Err.Error(), testCase.wantErrContains) } else { - assert.Equal(t, testCase.wantChannel, module.DesiredChannel) - assert.Equal(t, testCase.wantVersion, module.Spec.Version) + assert.Equal(t, testCase.wantChannel, moduleTemplateInfo.DesiredChannel) + assert.Equal(t, testCase.wantVersion, moduleTemplateInfo.Spec.Version) } } }) @@ -979,22 +1007,11 @@ func executeGetRegularTemplatesTestCases(t *testing.T, func generateModuleTemplateListWithModule(moduleName, moduleChannel, moduleVersion string) v1beta2.ModuleTemplateList { templateList := v1beta2.ModuleTemplateList{} templateList.Items = append(templateList.Items, *builder.NewModuleTemplateBuilder(). - WithName(fmt.Sprintf("%s-%s", moduleName, moduleVersion)). + WithName(v1beta2.CreateModuleTemplateName(moduleName, moduleVersion)). WithModuleName(moduleName). WithChannel(moduleChannel). WithVersion(moduleVersion). - WithDescriptor(&types.Descriptor{ - ComponentDescriptor: &compdesc.ComponentDescriptor{ - Metadata: compdesc.Metadata{ - ConfiguredVersion: compdescv2.SchemaVersion, - }, - ComponentSpec: compdesc.ComponentSpec{ - ObjectMeta: ocmmetav1.ObjectMeta{ - Version: moduleVersion, - }, - }, - }, - }).Build()) + Build()) return templateList } @@ -1004,6 +1021,7 @@ func generateModuleReleaseMetaList(moduleName string, mrmList := v1beta2.ModuleReleaseMetaList{} mrmList.Items = append(mrmList.Items, *builder.NewModuleReleaseMetaBuilder(). WithModuleName(moduleName). + WithOcmComponentName(testutils.FullOCMName(moduleName)). WithModuleChannelAndVersions(channelVersions). Build()) return mrmList @@ -1028,3 +1046,14 @@ func (mtlb *ModuleTemplateListBuilder) Build() v1beta2.ModuleTemplateList { func moduleToInstallByVersion(moduleName, moduleVersion string) v1beta2.Module { return testutils.NewTestModuleWithChannelVersion(moduleName, "", moduleVersion) } + +// registerEmptyComponentDescriptor registers an (almost) empty component descriptor with the given name and version. +func registerEmptyComponentDescriptor(fakeService *componentdescriptor.FakeService, name, version string) error { + cd := compdesc.New(name, version) + cdBytes, err := compdesc.Encode(cd) + if err != nil { + return err + } + fakeService.RegisterWithNameVersionOverride(name, version, cdBytes) + return nil +} diff --git a/pkg/testutils/builder/moduletemplate.go b/pkg/testutils/builder/moduletemplate.go index ff7a3ec8a4..d6fabe2204 100644 --- a/pkg/testutils/builder/moduletemplate.go +++ b/pkg/testutils/builder/moduletemplate.go @@ -117,11 +117,6 @@ func (m ModuleTemplateBuilder) WithRawDescriptor(rawDescriptor []byte) ModuleTem return m } -func (m ModuleTemplateBuilder) WithOCM(schemaVersion compdesc.SchemaVersion) ModuleTemplateBuilder { - m.moduleTemplate.Spec.Descriptor = ComponentDescriptorFactoryFromSchema(schemaVersion) - return m -} - func (m ModuleTemplateBuilder) WithRequiresDowntime(value bool) ModuleTemplateBuilder { m.moduleTemplate.Spec.RequiresDowntime = value return m diff --git a/pkg/testutils/kyma.go b/pkg/testutils/kyma.go index f49d819a41..427f604d07 100644 --- a/pkg/testutils/kyma.go +++ b/pkg/testutils/kyma.go @@ -63,6 +63,7 @@ func SyncKyma(ctx context.Context, clnt client.Client, kyma *v1beta2.Kyma) error }, kyma) // It might happen in some test case, kyma get deleted, if you need to make sure Kyma should exist, // write expected condition to check it specifically. + err = client.IgnoreNotFound(err) if err != nil { return fmt.Errorf("failed to fetch Kyma CR: %w", err) @@ -117,6 +118,29 @@ func DeleteKyma(ctx context.Context, return nil } +// UpdateKymaWithFunc uses the provided function to update the Kyma resource. +// This function is intended to be used with "Eventually" assertions in tests. +// The provided updateFn should modify the Kyma resource in place and return an error if the modification fails. +// UpdateKymaWithFunc always fetches the latest version of the Kyma resource before applying changes +// to make sure the update is based on the most recent state. +func UpdateKymaWithFunc(ctx context.Context, clnt client.Client, + kymaName, kymaNamespace string, updateFn func(kyma *v1beta2.Kyma) error, +) error { + kyma, err := GetKyma(ctx, clnt, kymaName, kymaNamespace) + if err != nil { + return fmt.Errorf("UpdateKymaWithFunc GetKyma: %w", err) + } + err = updateFn(kyma) + if err != nil { + return err + } + err = clnt.Update(ctx, kyma) + if err != nil { + return fmt.Errorf("UpdateKymaWithFunc client.Update: %w", err) + } + return nil +} + func KymaHasDeletionTimestamp(ctx context.Context, clnt client.Client, kymaName string, @@ -166,6 +190,8 @@ func EnableModule(ctx context.Context, return nil } +// DisableModule removes the module with the given name from the Kyma's spec.modules. +// If the module is not found, it does nothing. func DisableModule(ctx context.Context, clnt client.Client, kymaName, kymaNamespace, moduleName string, ) error { @@ -173,12 +199,16 @@ func DisableModule(ctx context.Context, clnt client.Client, if err != nil { return err } - for i, module := range kyma.Spec.Modules { - if module.Name == moduleName { - kyma.Spec.Modules = removeModuleWithIndex(kyma.Spec.Modules, i) - break + if len(kyma.Spec.Modules) == 0 { // no modules to disable + return nil + } + newModules := make([]v1beta2.Module, 0, len(kyma.Spec.Modules)) + for _, module := range kyma.Spec.Modules { + if module.Name != moduleName { + newModules = append(newModules, module) } } + kyma.Spec.Modules = newModules err = clnt.Update(ctx, kyma) if err != nil { return fmt.Errorf("update kyma: %w", err) @@ -206,10 +236,6 @@ func SetModuleManaged(ctx context.Context, clnt client.Client, return nil } -func removeModuleWithIndex(s []v1beta2.Module, index int) []v1beta2.Module { - return append(s[:index], s[index+1:]...) -} - func UpdateKymaModuleChannel(ctx context.Context, clnt client.Client, kymaName, kymaNamespace, channel string, ) error { diff --git a/pkg/testutils/modulereleasemeta.go b/pkg/testutils/modulereleasemeta.go index 6e3e6ad9c6..0ab7fe1fdd 100644 --- a/pkg/testutils/modulereleasemeta.go +++ b/pkg/testutils/modulereleasemeta.go @@ -8,6 +8,7 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/kyma-project/lifecycle-manager/api/shared" "github.com/kyma-project/lifecycle-manager/api/v1beta2" "github.com/kyma-project/lifecycle-manager/pkg/util" ) @@ -66,7 +67,8 @@ func GetModuleReleaseMeta(ctx context.Context, moduleName, namespace string, Name: moduleName, }, mrm) if err != nil { - return nil, fmt.Errorf("get kyma: %w", err) + return nil, fmt.Errorf("failed to get ModuleReleaseMeta"+ + " with name %q in namespace %q: %w", moduleName, namespace, err) } return mrm, nil } @@ -153,3 +155,9 @@ func MandatoryModuleReleaseMetaHasVersion(ctx context.Context, clnt client.Clien return fmt.Errorf("mandatory module %s not found", moduleName) } + +// FullOCMName returns the fully qualified OCM component name for a given module name. +// This is used by OCM-related functionality, end-users do not have to use this format. +func FullOCMName(moduleName string) string { + return shared.KymaGroup + "/module/" + moduleName +} diff --git a/pkg/testutils/moduletemplate.go b/pkg/testutils/moduletemplate.go index 58e381c926..f6ca0955cc 100644 --- a/pkg/testutils/moduletemplate.go +++ b/pkg/testutils/moduletemplate.go @@ -10,7 +10,7 @@ import ( "github.com/kyma-project/lifecycle-manager/api/shared" "github.com/kyma-project/lifecycle-manager/api/v1beta2" - "github.com/kyma-project/lifecycle-manager/internal/descriptor/provider" + "github.com/kyma-project/lifecycle-manager/internal/descriptor/types/ocmidentity" "github.com/kyma-project/lifecycle-manager/pkg/templatelookup" "github.com/kyma-project/lifecycle-manager/pkg/templatelookup/common" "github.com/kyma-project/lifecycle-manager/pkg/templatelookup/moduletemplateinfolookup" @@ -28,11 +28,11 @@ func CreateModuleTemplate(ctx context.Context, return nil } -func GetModuleTemplate(ctx context.Context, +func GetModuleTemplateInfo(ctx context.Context, clnt client.Client, module v1beta2.Module, kyma *v1beta2.Kyma, -) (*v1beta2.ModuleTemplate, error) { +) (*v1beta2.ModuleTemplate, *ocmidentity.ComponentId, error) { moduleTemplateInfoLookupStrategies := moduletemplateinfolookup.NewModuleTemplateInfoLookupStrategies( []moduletemplateinfolookup.ModuleTemplateInfoLookupStrategy{ moduletemplateinfolookup.NewByVersionStrategy(clnt), @@ -46,15 +46,17 @@ func GetModuleTemplate(ctx context.Context, moduleReleaseMeta, err := GetModuleReleaseMeta(ctx, module.Name, kyma.Namespace, clnt) if !meta.IsNoMatchError(err) && client.IgnoreNotFound(err) != nil { - return nil, fmt.Errorf("failed to get ModuleReleaseMeta: %w", err) + return nil, nil, fmt.Errorf("failed to get ModuleReleaseMeta: %w", err) } templateInfo := moduleTemplateInfoLookupStrategies.Lookup(ctx, &availableModule, kyma, moduleReleaseMeta) if templateInfo.Err != nil { - return nil, fmt.Errorf("get module template: %w", templateInfo.Err) + return nil, nil, fmt.Errorf("failed to get module template: %w", templateInfo.Err) } - return templateInfo.ModuleTemplate, nil + + ocmIdentity, err := templateInfo.GetOCMIdentity() + return templateInfo.ModuleTemplate, ocmIdentity, err } func ModuleTemplateExists(ctx context.Context, @@ -62,7 +64,7 @@ func ModuleTemplateExists(ctx context.Context, module v1beta2.Module, kyma *v1beta2.Kyma, ) error { - moduleTemplate, err := GetModuleTemplate(ctx, clnt, module, kyma) + moduleTemplate, _, err := GetModuleTemplateInfo(ctx, clnt, module, kyma) if moduleTemplate == nil || errors.Is(err, common.ErrNoTemplatesInListResult) { return ErrNotFound } @@ -103,10 +105,13 @@ func UpdateModuleTemplateSpec(ctx context.Context, newValue string, kyma *v1beta2.Kyma, ) error { - moduleTemplate, err := GetModuleTemplate(ctx, clnt, module, kyma) + moduleTemplate, _, err := GetModuleTemplateInfo(ctx, clnt, module, kyma) if err != nil { return err } + if moduleTemplate == nil { + return fmt.Errorf("%w: moduleTemplate is nil", ErrNotFound) + } if moduleTemplate.Spec.Data == nil { return ErrManifestResourceIsNil } @@ -117,13 +122,41 @@ func UpdateModuleTemplateSpec(ctx context.Context, return nil } +// UpdateModuleTemplateWithFunc uses the provided function to update the ModuleTemplate resource. +// This function is intended to be used with "Eventually" assertions in tests. +// The provided updateFn should modify the ModuleTemplate resource in place and return an error +// if the modification fails. +// UpdateModuleTemplateWithFunc fetches the latest version of the ModuleTemplate resource before applying changes, +// to make sure the update is based on the most recent state. +func UpdateModuleTemplateWithFunc(ctx context.Context, clnt client.Client, + mtName, mtNamespace string, updateFn func(mt *v1beta2.ModuleTemplate) error, +) error { + moduleTemplate := &v1beta2.ModuleTemplate{} + err := clnt.Get(ctx, client.ObjectKey{Name: mtName, Namespace: mtNamespace}, moduleTemplate) + if err != nil { + return fmt.Errorf("UpdateModuleTemplateWithFunc client.Get: %w", err) + } + err = updateFn(moduleTemplate) + if err != nil { + return err + } + err = clnt.Update(ctx, moduleTemplate) + if err != nil { + return fmt.Errorf("UpdateModuleTemplateWithFunc client.Update: %w", err) + } + return nil +} + func SetModuleTemplateBetaLabel(ctx context.Context, clnt client.Client, module v1beta2.Module, kyma *v1beta2.Kyma, betaValue bool, ) error { - moduleTemplate, err := GetModuleTemplate(ctx, clnt, module, kyma) + moduleTemplate, _, err := GetModuleTemplateInfo(ctx, clnt, module, kyma) if err != nil { return fmt.Errorf("failed to get module template: %w", err) } + if moduleTemplate == nil { + return fmt.Errorf("%w: moduleTemplate is nil", ErrNotFound) + } if moduleTemplate.Labels == nil { moduleTemplate.Labels = make(map[string]string) @@ -145,10 +178,13 @@ func SetModuleTemplateBetaLabel(ctx context.Context, clnt client.Client, module func SetModuleTemplateInternalLabel(ctx context.Context, clnt client.Client, module v1beta2.Module, kyma *v1beta2.Kyma, internalValue bool, ) error { - moduleTemplate, err := GetModuleTemplate(ctx, clnt, module, kyma) + moduleTemplate, _, err := GetModuleTemplateInfo(ctx, clnt, module, kyma) if err != nil { return fmt.Errorf("failed to get module template: %w", err) } + if moduleTemplate == nil { + return fmt.Errorf("%w: moduleTemplate is nil", ErrNotFound) + } if moduleTemplate.Labels == nil { moduleTemplate.Labels = make(map[string]string) @@ -197,7 +233,7 @@ func DeleteModuleTemplate(ctx context.Context, module v1beta2.Module, kyma *v1beta2.Kyma, ) error { - moduleTemplate, err := GetModuleTemplate(ctx, clnt, module, kyma) + moduleTemplate, _, err := GetModuleTemplateInfo(ctx, clnt, module, kyma) if util.IsNotFound(err) { return nil } @@ -209,21 +245,17 @@ func DeleteModuleTemplate(ctx context.Context, return nil } -func ReadModuleVersionFromModuleTemplate(ctx context.Context, +func GetOCMVersionForModule(ctx context.Context, clnt client.Client, module v1beta2.Module, kyma *v1beta2.Kyma, ) (string, error) { - moduleTemplate, err := GetModuleTemplate(ctx, clnt, module, kyma) + _, ocmIdentity, err := GetModuleTemplateInfo(ctx, clnt, module, kyma) if err != nil { return "", fmt.Errorf("failed to fetch ModuleTemplate: %w", err) } - - descriptorProvider := provider.NewCachedDescriptorProvider() - ocmDesc, err := descriptorProvider.GetDescriptor(moduleTemplate) - if err != nil { - return "", fmt.Errorf("failed to get descriptor: %w", err) + if ocmIdentity == nil { + return "", fmt.Errorf("failed to get OCM identity: %w", ErrNotFound) } - - return ocmDesc.Version, nil + return ocmIdentity.Version(), nil } diff --git a/pkg/testutils/ocm.go b/pkg/testutils/ocm.go index 8fe58e6799..690082701f 100644 --- a/pkg/testutils/ocm.go +++ b/pkg/testutils/ocm.go @@ -1,3 +1,16 @@ package testutils +import ( + "github.com/kyma-project/lifecycle-manager/internal/descriptor/types/ocmidentity" +) + const DefaultFQDN = "kyma-project.io/module/template-operator" + +// MustNewComponentId is a convenience ComponentId constructor that panics if name or version are not provided. +func MustNewComponentId(name, version string) *ocmidentity.ComponentId { + ocmi, err := ocmidentity.NewComponentId(name, version) + if err != nil { + panic(err) + } + return ocmi +} diff --git a/scripts/tests/deploy_mandatory_modulereleasemeta.sh b/scripts/tests/deploy_mandatory_modulereleasemeta.sh index 4fd6b6d5ba..3e98ab5d66 100755 --- a/scripts/tests/deploy_mandatory_modulereleasemeta.sh +++ b/scripts/tests/deploy_mandatory_modulereleasemeta.sh @@ -26,4 +26,4 @@ echo "Mandatory ModuleReleaseMeta created successfully" rm -f module-release-meta-mandatory.yaml kubectl get modulereleasemeta "${MODULE_NAME}" -n kcp-system -o yaml -kubectl get moduletemplate -n kcp-system +kubectl get moduletemplate -n kcp-system -o wide diff --git a/scripts/tests/deploy_moduletemplate.sh b/scripts/tests/deploy_moduletemplate.sh index 21e4961b36..81e2933b00 100755 --- a/scripts/tests/deploy_moduletemplate.sh +++ b/scripts/tests/deploy_moduletemplate.sh @@ -41,7 +41,6 @@ yq eval '.bdba += ["europe-docker.pkg.dev/kyma-project/prod/template-operator:'" cat module-config-for-e2e.yaml modulectl create --config-file ./module-config-for-e2e.yaml --registry http://localhost:5111 --insecure -sed -i 's/localhost:5111/k3d-kcp-registry.localhost:5000/g' ./template.yaml cat template.yaml echo "ModuleTemplate created successfully" diff --git a/tests/e2e/maintenance_windows_wait_test.go b/tests/e2e/maintenance_windows_wait_test.go index 619db44891..8c60112c52 100644 --- a/tests/e2e/maintenance_windows_wait_test.go +++ b/tests/e2e/maintenance_windows_wait_test.go @@ -159,7 +159,7 @@ var _ = Describe("Maintenance Windows - Wait for Maintenance Window", Ordered, f Should(Succeed()) By("And Kyma .status.modules[].version shows correct version") - newModuleTemplateVersion, err := ReadModuleVersionFromModuleTemplate(ctx, kcpClient, module, + newModuleTemplateVersion, err := GetOCMVersionForModule(ctx, kcpClient, module, kyma) Expect(err).ToNot(HaveOccurred()) diff --git a/tests/e2e/modulereleasemeta_module_upgrade_new_version_test.go b/tests/e2e/modulereleasemeta_module_upgrade_new_version_test.go index 11f504bed6..a1edba7762 100644 --- a/tests/e2e/modulereleasemeta_module_upgrade_new_version_test.go +++ b/tests/e2e/modulereleasemeta_module_upgrade_new_version_test.go @@ -77,7 +77,7 @@ var _ = Describe("Module with ModuleReleaseMeta Upgrade By New Version", Ordered Should(Succeed()) By("And Kyma Module Version in Kyma Status is updated") - newModuleTemplateVersion, err := ReadModuleVersionFromModuleTemplate(ctx, kcpClient, module, + newModuleTemplateVersion, err := GetOCMVersionForModule(ctx, kcpClient, module, kyma) Expect(err).ToNot(HaveOccurred()) diff --git a/tests/integration/controller/kcp/helper_test.go b/tests/integration/controller/kcp/helper_test.go index 4c700da125..b961316e5a 100644 --- a/tests/integration/controller/kcp/helper_test.go +++ b/tests/integration/controller/kcp/helper_test.go @@ -7,17 +7,15 @@ import ( apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - compdescv2 "ocm.software/ocm/api/ocm/compdesc/versions/v2" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/kyma-project/lifecycle-manager/api/shared" "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" - - . "github.com/kyma-project/lifecycle-manager/pkg/testutils" ) var ( @@ -41,7 +39,6 @@ const ( func registerControlPlaneLifecycleForKyma(kyma *v1beta2.Kyma) { BeforeAll(func() { - DeployModuleTemplates(ctx, kcpClient, kyma) Eventually(CreateCR, Timeout, Interval). WithContext(ctx). WithArguments(kcpClient, kyma).Should(Succeed()) @@ -61,16 +58,23 @@ func registerControlPlaneLifecycleForKyma(kyma *v1beta2.Kyma) { }) } +// Note: Uses simplified logic and deletes ALL versions of the ModuleTemplates +// configured in the Kyma spec. +// For precise deletion one has to find the correct ModuleTemplate instance based on +// the version and channel mapping specified in a corresponding ModuleReleaseMeta. func DeleteModuleTemplates(ctx context.Context, kcpClient client.Client, kyma *v1beta2.Kyma) { for _, module := range kyma.Spec.Modules { - template := builder.NewModuleTemplateBuilder(). - WithNamespace(ControlPlaneNamespace). - WithModuleName(module.Name). - WithChannel(module.Channel). - WithOCM(compdescv2.SchemaVersion).Build() - Eventually(DeleteCR, Timeout, Interval). - WithContext(ctx). - WithArguments(kcpClient, template).Should(Succeed()) + allVersionsOfAModule := &v1beta2.ModuleTemplateList{} + listOpts := []client.ListOption{ + client.InNamespace(ControlPlaneNamespace), + client.MatchingLabels{shared.ModuleName: module.Name}, + } + Expect(kcpClient.List(ctx, allVersionsOfAModule, listOpts...)).Should(Succeed()) + for _, mtInstance := range allVersionsOfAModule.Items { + Eventually(DeleteCR, Timeout, Interval). + WithContext(ctx). + WithArguments(kcpClient, &mtInstance).Should(Succeed()) + } } } @@ -85,8 +89,8 @@ func DeployModuleTemplates(ctx context.Context, kcpClient client.Client, kyma *v WithNamespace(ControlPlaneNamespace). WithModuleName(module.Name). WithChannel(module.Channel). - WithOCM(compdescv2.SchemaVersion). - WithName(moduleTemplateName).Build() + WithName(moduleTemplateName). + Build() Eventually(kcpClient.Create, Timeout, Interval).WithContext(ctx). WithArguments(template). Should(Succeed()) @@ -126,7 +130,7 @@ func expectModuleTemplateSpecGetReset( module v1beta2.Module, kyma *v1beta2.Kyma, ) error { - moduleTemplate, err := GetModuleTemplate(ctx, clnt, module, kyma) + moduleTemplate, _, err := GetModuleTemplateInfo(ctx, clnt, module, kyma) if err != nil { return err } diff --git a/tests/integration/controller/kcp/module_installation_test.go b/tests/integration/controller/kcp/module_installation_test.go index 306f60a877..1f8bddb3d8 100644 --- a/tests/integration/controller/kcp/module_installation_test.go +++ b/tests/integration/controller/kcp/module_installation_test.go @@ -6,7 +6,6 @@ import ( "fmt" "k8s.io/apimachinery/pkg/types" - compdescv2 "ocm.software/ocm/api/ocm/compdesc/versions/v2" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/kyma-project/lifecycle-manager/api/shared" @@ -37,8 +36,10 @@ var _ = Describe("Module installation", func() { moduleInternal).Should(Succeed()) Eventually(configureKCPModuleReleaseMeta, Timeout, Interval).WithArguments(moduleName).Should(Succeed()) + err := registerDescriptor(FullOCMName(moduleName), moduleVersion) + Expect(err).ShouldNot(HaveOccurred()) + var skrClient client.Client - var err error Eventually(func() error { skrClient, err = testSkrContextFactory.Get(types.NamespacedName{ Name: kymaName, @@ -219,7 +220,6 @@ func configureKCPModuleTemplates(moduleName string, moduleBeta, moduleInternal b WithName(fmt.Sprintf("%s-%s", moduleName, moduleVersion)). WithModuleName(moduleName). WithVersion(moduleVersion). - WithOCM(compdescv2.SchemaVersion). WithBeta(moduleBeta). WithInternal(moduleInternal). Build() @@ -236,6 +236,7 @@ func configureKCPModuleReleaseMeta(moduleName string) error { moduleReleaseMeta := builder.NewModuleReleaseMetaBuilder(). WithNamespace(ControlPlaneNamespace). WithModuleName(moduleName). + WithOcmComponentName(FullOCMName(moduleName)). WithSingleModuleChannelAndVersions(v1beta2.DefaultChannel, moduleVersion). Build() diff --git a/tests/integration/controller/kcp/remote_sync_test.go b/tests/integration/controller/kcp/remote_sync_test.go index 1e9afbdde0..c1a8eaa05d 100644 --- a/tests/integration/controller/kcp/remote_sync_test.go +++ b/tests/integration/controller/kcp/remote_sync_test.go @@ -10,11 +10,12 @@ import ( apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - compdescv2 "ocm.software/ocm/api/ocm/compdesc/versions/v2" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/kyma-project/lifecycle-manager/api/shared" "github.com/kyma-project/lifecycle-manager/api/v1beta2" + + "github.com/kyma-project/lifecycle-manager/internal/descriptor/types/ocmidentity" "github.com/kyma-project/lifecycle-manager/internal/pkg/flags" "github.com/kyma-project/lifecycle-manager/internal/util/collections" "github.com/kyma-project/lifecycle-manager/pkg/testutils/builder" @@ -33,39 +34,32 @@ var ( ) var _ = Describe("Kyma sync into Remote Cluster", Ordered, func() { + var err error kyma := NewTestKyma("kyma-1") skrKyma := NewSKRKyma() - moduleInSKR := NewTestModuleWithChannelVersion("skr-module", v1beta2.DefaultChannel, "0.1.0") - moduleInKCP := NewTestModuleWithChannelVersion("kcp-module", v1beta2.DefaultChannel, "0.1.0") + moduleInSKR := NewTestModule("skrmodule", v1beta2.DefaultChannel) + moduleInKCP := NewTestModule("kcpmodule", v1beta2.DefaultChannel) defaultCR := builder.NewModuleCRBuilder().WithSpec(InitSpecKey, InitSpecValue).Build() - ModuleReleaseMetaKcp := builder.NewModuleReleaseMetaBuilder(). - WithNamespace(ControlPlaneNamespace). - WithName(moduleInKCP.Name). - WithModuleName(moduleInKCP.Name). - WithSingleModuleChannelAndVersions(moduleInKCP.Channel, moduleInKCP.Version).Build() - ModuleReleaseMetaSkr := builder.NewModuleReleaseMetaBuilder(). - WithNamespace(ControlPlaneNamespace). - WithName(moduleInSKR.Name). - WithModuleName(moduleInSKR.Name). - WithSingleModuleChannelAndVersions(moduleInSKR.Channel, moduleInSKR.Version).Build() + moduleInSKROCMName := FullOCMName(moduleInSKR.Name) + moduleInSKROCM := MustNewComponentId(moduleInSKROCMName, moduleVersion) + moduleInKCPOCMName := FullOCMName(moduleInKCP.Name) + TemplateForSKREnabledModule := builder.NewModuleTemplateBuilder(). + WithName(v1beta2.CreateModuleTemplateName(moduleInSKR.Name, moduleVersion)). WithNamespace(ControlPlaneNamespace). - WithName(v1beta2.CreateModuleTemplateName(moduleInSKR.Name, moduleInSKR.Version)). WithModuleName(moduleInSKR.Name). - WithVersion(moduleInSKR.Version). - WithChannel(moduleInSKR.Channel). + WithVersion(moduleVersion). WithModuleCR(defaultCR). - WithOCM(compdescv2.SchemaVersion).Build() + Build() + TemplateForKCPEnabledModule := builder.NewModuleTemplateBuilder(). + WithName(v1beta2.CreateModuleTemplateName(moduleInKCP.Name, moduleVersion)). WithNamespace(ControlPlaneNamespace). - WithName(v1beta2.CreateModuleTemplateName(moduleInKCP.Name, moduleInKCP.Version)). WithModuleName(moduleInKCP.Name). - WithVersion(moduleInKCP.Version). - WithChannel(moduleInKCP.Channel). + WithVersion(moduleVersion). WithModuleCR(defaultCR). - WithOCM(compdescv2.SchemaVersion).Build() + Build() var skrClient client.Client - var err error BeforeAll(func() { Eventually(CreateCR, Timeout, Interval). WithContext(ctx). @@ -113,15 +107,13 @@ var _ = Describe("Kyma sync into Remote Cluster", Ordered, func() { Should(Succeed()) }) - It("KCP ModuleReleaseMeta should be created", func() { - Eventually(CreateModuleReleaseMeta, Timeout, Interval). - WithContext(ctx). - WithArguments(kcpClient, ModuleReleaseMetaKcp). - Should(Succeed()) - Eventually(CreateModuleReleaseMeta, Timeout, Interval). - WithContext(ctx). - WithArguments(kcpClient, ModuleReleaseMetaSkr). - Should(Succeed()) + It("ModuleReleaseMeta should be created in KCP", func() { + err := registerDescriptor(moduleInSKROCMName, moduleVersion) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(configureKCPModuleReleaseMeta, Timeout, Interval).WithArguments(moduleInSKR.Name).Should(Succeed()) + err = registerDescriptor(moduleInKCPOCMName, moduleVersion) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(configureKCPModuleReleaseMeta, Timeout, Interval).WithArguments(moduleInKCP.Name).Should(Succeed()) }) It("ModuleTemplates should be synchronized in both clusters", func() { @@ -190,8 +182,8 @@ var _ = Describe("Kyma sync into Remote Cluster", Ordered, func() { WithArguments(kcpClient, kyma.GetName(), kyma.GetNamespace(), moduleInSKR.Name, shared.StateReady). Should(Succeed()) - By("ModuleTemplate descriptor should be saved in cache") - Expect(IsDescriptorCached(TemplateForSKREnabledModule)).Should(BeTrue()) + By("component descriptor should be saved in cache") + Expect(IsDescriptorCached(*moduleInSKROCM)).Should(BeTrue()) By("Remote Kyma contains correct conditions for Modules") Eventually(kymaHasCondition, Timeout, Interval). @@ -247,19 +239,35 @@ var _ = Describe("Kyma sync into Remote Cluster", Ordered, func() { }) }) -func IsDescriptorCached(template *v1beta2.ModuleTemplate) bool { - key := descriptorProvider.GenerateDescriptorKey(template.Name, template.Spec.Version) - result := descriptorProvider.DescriptorCache.Get(key) - return result != nil +// IsDescriptorCached checks if the descriptor is in the cache. +// It temporarily stops the underlying DescriptorService to ensure the cache is used +// instead of DescriptorService lookup. +func IsDescriptorCached(ocmi ocmidentity.ComponentId) bool { + descProviderService.Stop() + defer descProviderService.Resume() + result, err := descriptorProvider.GetDescriptor(ocmi) + return err == nil && result != nil } var _ = Describe("Kyma sync default module list into Remote Cluster", Ordered, func() { - kyma := NewTestKyma("kyma-2") - moduleInKCP := NewTestModule("kcp-module", v1beta2.DefaultChannel) - kyma.Spec.Modules = append(kyma.Spec.Modules, moduleInKCP) - skrKyma := NewSKRKyma() var skrClient client.Client var err error + + kyma := NewTestKyma("kyma-2") + skrKyma := NewSKRKyma() + moduleInKCP := NewTestModule("kcpmodule", v1beta2.DefaultChannel) + kyma.Spec.Modules = append(kyma.Spec.Modules, moduleInKCP) + + moduleInKCPOCMName := FullOCMName(moduleInKCP.Name) + + templateForModuleInKCP := builder.NewModuleTemplateBuilder(). + WithName(fmt.Sprintf("%s-%s", moduleInKCP.Name, moduleVersion)). + WithNamespace(ControlPlaneNamespace). + WithModuleName(moduleInKCP.Name). + WithChannel(moduleInKCP.Channel). + WithVersion(moduleVersion). + Build() + registerControlPlaneLifecycleForKyma(kyma) BeforeAll(func() { Eventually(func() error { @@ -268,6 +276,20 @@ var _ = Describe("Kyma sync default module list into Remote Cluster", Ordered, f }, Timeout, Interval).Should(Succeed()) }) + It("ModuleTemplate for default module should be created in KCP", func() { + Eventually(CreateModuleTemplate, Timeout, Interval). + WithContext(ctx). + WithArguments(kcpClient, templateForModuleInKCP). + Should(Succeed()) + }) + + It("ModuleReleaseMeta should be created in KCP", func() { + err := registerDescriptor(moduleInKCPOCMName, moduleVersion) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(configureKCPModuleReleaseMeta, Timeout, Interval). + WithArguments(moduleInKCP.Name).Should(Succeed()) + }) + It("Kyma CR default module list should be copied to remote Kyma", func() { By("Remote Kyma created") Eventually(KymaExists, Timeout, Interval). @@ -327,7 +349,7 @@ var _ = Describe("Kyma sync default module list into Remote Cluster", Ordered, f var _ = Describe("CRDs sync to SKR and annotations updated in KCP kyma", Ordered, func() { kyma := NewTestKyma("kyma-test-crd-update") moduleInKCP := NewTestModuleWithChannelVersion("module-inkcp", v1beta2.DefaultChannel, "0.1.0") - moduleTemplateName := v1beta2.CreateModuleTemplateName(moduleInKCP.Name, "0.1.0") + moduleTemplateName := v1beta2.CreateModuleTemplateName(moduleInKCP.Name, moduleInKCP.Version) moduleReleaseMetaInKCP := builder.NewModuleReleaseMetaBuilder(). WithName("modulereleasemeta-inkcp"). @@ -345,13 +367,11 @@ var _ = Describe("CRDs sync to SKR and annotations updated in KCP kyma", Ordered var err error BeforeAll(func() { template := builder.NewModuleTemplateBuilder(). - WithName(v1beta2.CreateModuleTemplateName(moduleInKCP.Name, moduleInKCP.Version)). + WithName(moduleTemplateName). WithNamespace(ControlPlaneNamespace). WithModuleName(moduleInKCP.Name). WithVersion(moduleInKCP.Version). - WithChannel(moduleInKCP.Channel). - WithOCM(compdescv2.SchemaVersion). - WithName(moduleTemplateName).Build() + Build() Eventually(kcpClient.Create, Timeout, Interval).WithContext(ctx). WithArguments(template). Should(Succeed()) diff --git a/tests/integration/controller/kcp/suite_test.go b/tests/integration/controller/kcp/suite_test.go index 153584f580..ba320c5277 100644 --- a/tests/integration/controller/kcp/suite_test.go +++ b/tests/integration/controller/kcp/suite_test.go @@ -23,6 +23,7 @@ import ( "time" certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + "github.com/kyma-project/lifecycle-manager/pkg/testutils/builder" "go.uber.org/zap/zapcore" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" machineryaml "k8s.io/apimachinery/pkg/util/yaml" @@ -45,6 +46,7 @@ import ( "github.com/kyma-project/lifecycle-manager/internal/pkg/flags" "github.com/kyma-project/lifecycle-manager/internal/pkg/metrics" "github.com/kyma-project/lifecycle-manager/internal/remote" + "github.com/kyma-project/lifecycle-manager/internal/service/componentdescriptor" "github.com/kyma-project/lifecycle-manager/internal/service/kyma/status/modules" "github.com/kyma-project/lifecycle-manager/internal/service/kyma/status/modules/generator" "github.com/kyma-project/lifecycle-manager/internal/service/kyma/status/modules/generator/fromerror" @@ -55,6 +57,7 @@ import ( "github.com/kyma-project/lifecycle-manager/pkg/templatelookup/moduletemplateinfolookup" "github.com/kyma-project/lifecycle-manager/tests/integration" testskrcontext "github.com/kyma-project/lifecycle-manager/tests/integration/commontestutils/skrcontextimpl" + compdescv2 "ocm.software/ocm/api/ocm/compdesc/versions/v2" _ "ocm.software/ocm/api/ocm" @@ -77,7 +80,9 @@ var ( cancel context.CancelFunc cfg *rest.Config descriptorProvider *provider.CachedDescriptorProvider + descProviderService *componentdescriptor.FakeService crdCache *crd.Cache + registerDescriptor func(name, version string) error // register component descriptors during tests. ) func TestAPIs(t *testing.T) { @@ -149,7 +154,16 @@ var _ = BeforeSuite(func() { testEventRec := event.NewRecorderWrapper(mgr.GetEventRecorderFor(shared.OperatorName)) testSkrContextFactory = testskrcontext.NewDualClusterFactory(kcpClient.Scheme(), testEventRec) - descriptorProvider = provider.NewCachedDescriptorProvider() + compDescrawBytes := builder.ComponentDescriptorFactoryFromSchema(compdescv2.SchemaVersion) + descProviderService = &componentdescriptor.FakeService{} + registerDescriptor = func(name, version string) error { + descProviderService.RegisterWithNameVersionOverride(name, version, compDescrawBytes.Raw) + return nil + } + + Expect(err).ToNot(HaveOccurred()) + descriptorProvider = provider.NewCachedDescriptorProvider(descProviderService) + crdCache = crd.NewCache(nil) noOpMetricsFunc := func(kymaName, moduleName string) {} moduleStatusGen := generator.NewModuleStatusGenerator(fromerror.GenerateModuleStatusFromError) diff --git a/tests/integration/controller/kyma/helper_test.go b/tests/integration/controller/kyma/helper_test.go index c4c31f3721..a32fa41ce9 100644 --- a/tests/integration/controller/kyma/helper_test.go +++ b/tests/integration/controller/kyma/helper_test.go @@ -3,9 +3,8 @@ package kyma_test import ( "context" "encoding/json" - "fmt" + "time" - compdescv2 "ocm.software/ocm/api/ocm/compdesc/versions/v2" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/kyma-project/lifecycle-manager/api/shared" @@ -20,21 +19,29 @@ import ( ) const ( - InitSpecKey = "initKey" - InitSpecValue = "initValue" - mandatoryChannel = "dummychannel" + InitSpecKey = "initKey" + InitSpecValue = "initValue" + ver101 = "1.0.1" + ver201 = "2.0.1" ) func RegisterDefaultLifecycleForKyma(kyma *v1beta2.Kyma) { + const mandatoryModuleName = "mandatory-module" + const normalModuleVersion = ver101 + const mandatoryModuleVersion = ver201 + RegisterDefaultLifecycleForKymaWithoutTemplate(kyma) + objTracker := &deletionTracker{} BeforeAll(func() { - DeployMandatoryModuleTemplate(ctx, kcpClient) - DeployModuleTemplates(ctx, kcpClient, kyma) + DeployMandatoryModuleTemplate(ctx, kcpClient, mandatoryModuleName, mandatoryModuleVersion, objTracker) + DeployModuleTemplates(ctx, kcpClient, kyma, normalModuleVersion, objTracker) }) AfterAll(func() { - DeleteModuleTemplates(ctx, kcpClient, kyma) - DeleteMandatoryModuleTemplate(ctx, kcpClient) + Eventually(objTracker.tryDeleteAll, Timeout, Interval). + WithContext(ctx). + WithArguments(kcpClient). + Should(Succeed()) }) } @@ -58,31 +65,30 @@ func RegisterDefaultLifecycleForKymaWithoutTemplate(kyma *v1beta2.Kyma) { }) } -func DeleteModuleTemplates(ctx context.Context, kcpClient client.Client, kyma *v1beta2.Kyma) { +// DeployModuleTemplates deploys ModuleTemplate and ModuleReleaseMeta for each module in the given Kyma spec. +// It also registers the corresponding OCM descriptors (default empty ones). +// The created resources are tracked by the provided deletionTracker for later cleanup. +func DeployModuleTemplates(ctx context.Context, kcpClient client.Client, kyma *v1beta2.Kyma, + version string, tracker *deletionTracker) { for _, module := range kyma.Spec.Modules { template := builder.NewModuleTemplateBuilder(). + WithName(v1beta2.CreateModuleTemplateName(module.Name, version)). WithNamespace(ControlPlaneNamespace). - WithName(createModuleTemplateName(module)). - WithModuleName(module.Name). - WithChannel(module.Channel). - WithOCM(compdescv2.SchemaVersion).Build() - Eventually(DeleteCR, Timeout, Interval). - WithContext(ctx). - WithArguments(kcpClient, template).Should(Succeed()) - } -} - -func DeployModuleTemplates(ctx context.Context, kcpClient client.Client, kyma *v1beta2.Kyma) { - for _, module := range kyma.Spec.Modules { - template := builder.NewModuleTemplateBuilder(). - WithName(createModuleTemplateName(module)). WithModuleName(module.Name). - WithNamespace(ControlPlaneNamespace). - WithChannel(module.Channel). - WithOCM(compdescv2.SchemaVersion).Build() + WithVersion(version). + Build() Eventually(CreateCR, Timeout, Interval).WithContext(ctx). WithArguments(kcpClient, template). Should(Succeed()) + defer tracker.add(template) + moduleReleaseMeta := ConfigureKCPModuleReleaseMeta(module.Name, module.Channel, version) + Eventually(CreateCR, Timeout, Interval).WithContext(ctx). + WithArguments(kcpClient, moduleReleaseMeta). + Should(Succeed()) + defer tracker.add(moduleReleaseMeta) + err := registerDescriptor(moduleReleaseMeta.Spec.OcmComponentName, version) + Expect(err).ShouldNot(HaveOccurred()) + managedModule := NewTestModuleWithFixName(module.Name, module.Channel, "") Eventually(ModuleTemplateExists, Timeout, Interval). WithArguments(ctx, kcpClient, managedModule, kyma). @@ -90,32 +96,33 @@ func DeployModuleTemplates(ctx context.Context, kcpClient client.Client, kyma *v } } -func DeployMandatoryModuleTemplate(ctx context.Context, kcpClient client.Client) { - mandatoryTemplate := newMandatoryModuleTemplate() +// DeployMandatoryModuleTemplate deploys a mandatory ModuleTemplate and its corresponding ModuleReleaseMeta. +// It also registers the corresponding OCM descriptor (default empty one). +// The created resources are tracked by the provided deletionTracker for later cleanup. +func DeployMandatoryModuleTemplate(ctx context.Context, kcpClient client.Client, moduleName, + version string, tracker *deletionTracker) { + mandatoryTemplate := newMandatoryModuleTemplate(moduleName, version) Eventually(CreateCR, Timeout, Interval). WithContext(ctx). WithArguments(kcpClient, mandatoryTemplate).Should(Succeed()) + defer tracker.add(mandatoryTemplate) + moduleReleaseMeta := ConfigureKCPMandatoryModuleReleaseMeta(mandatoryTemplate.Spec.ModuleName, version) + Eventually(CreateCR, Timeout, Interval).WithContext(ctx). + WithArguments(kcpClient, moduleReleaseMeta). + Should(Succeed()) + defer tracker.add(moduleReleaseMeta) + err := registerDescriptor(moduleReleaseMeta.Spec.OcmComponentName, version) + Expect(err).ShouldNot(HaveOccurred()) } -func DeleteMandatoryModuleTemplate(ctx context.Context, kcpClient client.Client) { - mandatoryTemplate := newMandatoryModuleTemplate() - Eventually(DeleteCR, Timeout, Interval). - WithContext(ctx). - WithArguments(kcpClient, mandatoryTemplate).Should(Succeed()) -} - -func createModuleTemplateName(module v1beta2.Module) string { - return fmt.Sprintf("%s-%s", module.Name, module.Channel) -} - -func newMandatoryModuleTemplate() *v1beta2.ModuleTemplate { +func newMandatoryModuleTemplate(moduleName, version string) *v1beta2.ModuleTemplate { return builder.NewModuleTemplateBuilder(). + WithName(v1beta2.CreateModuleTemplateName(moduleName, version)). WithNamespace(ControlPlaneNamespace). - WithName("mandatory-template"). - WithModuleName("mandatory-template-operator"). - WithChannel(mandatoryChannel). + WithModuleName(moduleName). + WithVersion(version). WithMandatory(true). - WithOCM(compdescv2.SchemaVersion).Build() + Build() } func KCPModuleExistWithOverwrites(kyma *v1beta2.Kyma, module v1beta2.Module) string { @@ -146,3 +153,45 @@ func UpdateAllManifestState(kymaName, kymaNamespace string, state shared.State) return nil } } + +func ConfigureKCPModuleReleaseMeta(moduleName, moduleChannel, moduleVersion string) *v1beta2.ModuleReleaseMeta { + return builder.NewModuleReleaseMetaBuilder(). + WithNamespace(ControlPlaneNamespace). + WithModuleName(moduleName). + WithOcmComponentName(FullOCMName(moduleName)). + WithSingleModuleChannelAndVersions(moduleChannel, moduleVersion). + Build() +} + +func ConfigureKCPMandatoryModuleReleaseMeta(moduleName, moduleVersion string) *v1beta2.ModuleReleaseMeta { + return builder.NewModuleReleaseMetaBuilder(). + WithNamespace(ControlPlaneNamespace). + WithModuleName(moduleName). + WithOcmComponentName(FullOCMName(moduleName)). + WithMandatory(moduleVersion). + Build() +} + +// deletionTracker helps to track created objects and delete them in the end of the test. +// Introduced because manual deletion was very fragile because of big number of independent test cases +// that actually depend on creation and deletion of similar objects. +type deletionTracker struct { + objects []client.Object +} + +func (dt *deletionTracker) add(obj client.Object) { + dt.objects = append(dt.objects, obj) +} + +// tryDeleteAll tries to delete all tracked objects and returns on the first error. +// The remaining objects (including the one for which the deletion failed) are kept for the next try. +func (dt *deletionTracker) tryDeleteAll(ctx context.Context, kcpClient client.Client) error { + for i, obj := range dt.objects { + if err := kcpClient.Delete(ctx, obj); err != nil && client.IgnoreNotFound(err) != nil { + dt.objects = dt.objects[i:] // keep the rest for next try + return err + } + time.Sleep(50 * time.Millisecond) // slight delay to avoid overwhelming the API server + } + return nil +} diff --git a/tests/integration/controller/kyma/kyma_module_channel_test.go b/tests/integration/controller/kyma/kyma_module_channel_test.go index c0d60db017..19368db36d 100644 --- a/tests/integration/controller/kyma/kyma_module_channel_test.go +++ b/tests/integration/controller/kyma/kyma_module_channel_test.go @@ -1,17 +1,18 @@ package kyma_test import ( + "context" "errors" "fmt" apierrors "k8s.io/apimachinery/pkg/api/errors" apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "ocm.software/ocm/api/ocm/compdesc" - compdescv2 "ocm.software/ocm/api/ocm/compdesc/versions/v2" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/kyma-project/lifecycle-manager/api/shared" "github.com/kyma-project/lifecycle-manager/api/v1beta2" + "github.com/kyma-project/lifecycle-manager/pkg/testutils/builder" . "github.com/onsi/ginkgo/v2" @@ -105,7 +106,7 @@ var _ = Describe("module channel different from the global channel", Ordered, fu Expect(kyma.Spec.Channel).Should(Equal(ValidChannel)) Expect(skrKyma.Spec.Channel).Should(Equal(ValidChannel)) }) - It("should enable standard modules in a valid channel in SKR Kyma", func() { + It("should enable standard modules in a fast channel in SKR Kyma", func() { Eventually(EnableModule, Timeout, Interval). WithContext(ctx). WithArguments(skrClient, skrKyma.GetName(), skrKyma.GetNamespace(), moduleInFastChannel). @@ -115,6 +116,7 @@ var _ = Describe("module channel different from the global channel", Ordered, fu Eventually(deployModuleInChannel).WithArguments(FastChannel, moduleName).Should(Succeed()) }) It("Manifest should be deployed in fast channel", func() { + Eventually(expectModuleManifestToHaveChannel, Timeout, Interval).WithArguments( kyma.GetName(), kyma.GetNamespace(), moduleName, FastChannel).Should(Succeed()) }) @@ -166,15 +168,13 @@ var _ = Describe("Given invalid channel which is rejected by CRD validation rule func givenModuleTemplateWithChannel(channel string, isValid bool) func() error { return func() error { - modules := []v1beta2.Module{ - { - ControllerName: "manifest", - Name: "module-with-" + channel, - Channel: channel, - Managed: true, - }, + module := v1beta2.Module{ + ControllerName: "manifest", + Name: "module-with-" + channel, + Channel: channel, + Managed: true, } - err := createModuleTemplateSetsForKyma(modules, LowerVersion, channel) + err := createModuleTemplateSetsForKyma(module.Name, LowerVersion, channel) if isValid { return err } @@ -183,15 +183,13 @@ func givenModuleTemplateWithChannel(channel string, isValid bool) func() error { } func deployModuleInChannel(channel string, moduleName string) error { - modules := []v1beta2.Module{ - { - ControllerName: "manifest", - Name: moduleName, - Channel: channel, - Managed: true, - }, + module := v1beta2.Module{ + ControllerName: "manifest", + Name: moduleName, + Channel: channel, + Managed: true, } - err := createModuleTemplateSetsForKyma(modules, LowerVersion, channel) + err := createModuleTemplateSetsForKyma(module.Name, LowerVersion, channel) return err } @@ -232,20 +230,18 @@ func givenKymaSpecModulesWithInvalidChannel(channel string) func() error { var _ = Describe("Channel switch", Ordered, func() { kyma := NewTestKyma("empty-module-kyma") skrKyma := NewSKRKyma() - modules := []v1beta2.Module{ - { - ControllerName: "manifest", - Name: "channel-switch", - Channel: v1beta2.DefaultChannel, - Managed: true, - }, + module := v1beta2.Module{ + ControllerName: "manifest", + Name: "channel-switch", + Channel: v1beta2.DefaultChannel, + Managed: true, } var skrClient client.Client var err error BeforeAll(func() { - Expect(createModuleTemplateSetsForKyma(modules, LowerVersion, v1beta2.DefaultChannel)).To(Succeed()) - Expect(createModuleTemplateSetsForKyma(modules, HigherVersion, FastChannel)).To(Succeed()) + Expect(createModuleTemplateSetsForKyma(module.Name, LowerVersion, v1beta2.DefaultChannel)).To(Succeed()) + Expect(createModuleTemplateSetsForKyma(module.Name, HigherVersion, FastChannel)).To(Succeed()) Eventually(CreateCR, Timeout, Interval). WithContext(ctx). WithArguments(kcpClient, kyma).Should(Succeed()) @@ -255,7 +251,7 @@ var _ = Describe("Channel switch", Ordered, func() { }, Timeout, Interval).Should(Succeed()) }) AfterAll(func() { - CleanupModuleTemplateSetsForKyma(kyma) + CleanupModuleTemplateSetsForKyma(ctx, kyma) Eventually(DeleteCR, Timeout, Interval). WithContext(ctx). WithArguments(kcpClient, kyma).Should(Succeed()) @@ -281,7 +277,7 @@ var _ = Describe("Channel switch", Ordered, func() { It("Standard Modules are enabled in default channel normally", func() { Eventually(EnableModule, Timeout, Interval). WithContext(ctx). - WithArguments(skrClient, skrKyma.GetName(), skrKyma.GetNamespace(), modules[0]). + WithArguments(skrClient, skrKyma.GetName(), skrKyma.GetNamespace(), module). Should(Succeed()) }) It("should create kyma with standard modules in default channel normally", func() { @@ -374,31 +370,38 @@ var _ = Describe("Channel switch", Ordered, func() { }, ) -func CleanupModuleTemplateSetsForKyma(kyma *v1beta2.Kyma) func() { +func CleanupModuleTemplateSetsForKyma(ctx context.Context, kyma *v1beta2.Kyma) func() { return func() { - By("Cleaning up decremented ModuleTemplate set in regular") - for _, module := range kyma.Spec.Modules { - template := builder.NewModuleTemplateBuilder(). - WithNamespace(ControlPlaneNamespace). - WithName(fmt.Sprintf("%s-%s", module.Name, v1beta2.DefaultChannel)). - WithModuleName(module.Name). - WithChannel(module.Channel). - WithOCM(compdescv2.SchemaVersion).Build() - Eventually(DeleteCR, Timeout, Interval). - WithContext(ctx). - WithArguments(kcpClient, template).Should(Succeed()) - } - By("Cleaning up standard ModuleTemplate set in fast") for _, module := range kyma.Spec.Modules { - template := builder.NewModuleTemplateBuilder(). - WithNamespace(ControlPlaneNamespace). - WithName(fmt.Sprintf("%s-%s", module.Name, FastChannel)). - WithModuleName(module.Name). - WithChannel(module.Channel). - WithOCM(compdescv2.SchemaVersion).Build() - Eventually(DeleteCR, Timeout, Interval). - WithContext(ctx). - WithArguments(kcpClient, template).Should(Succeed()) + By("Cleaning up for module: " + module.Name) + var mrmToDelete *v1beta2.ModuleReleaseMeta + var err error + Eventually(func() error { + mrmToDelete, err = GetModuleReleaseMeta(ctx, module.Name, ControlPlaneNamespace, kcpClient) + if err != nil { + return err + } + return nil + }, Timeout, Interval).Should(Succeed()) + + if mrmToDelete.Spec.Mandatory != nil && mrmToDelete.Spec.Mandatory.Version != "" { + Fail("mandatory modules are not expected") + } + + By(" - Deleting ModuleTemplates") + for _, channelVersion := range mrmToDelete.Spec.Channels { + templateToDelete := &v1beta2.ModuleTemplate{} + templateToDelete.Namespace = ControlPlaneNamespace + templateToDelete.Name = fmt.Sprintf("%s-%s", module.Name, channelVersion.Version) + Eventually(func() error { + return kcpClient.Delete(ctx, templateToDelete) + }, Timeout, Interval).Should(Succeed()) + } + + By(" - Deleting ModuleReleaseMeta") + Eventually(func() error { + return kcpClient.Delete(ctx, mrmToDelete) + }, Timeout, Interval).Should(Succeed()) } } } @@ -484,29 +487,43 @@ func whenUpdatingEveryModuleChannel(clnt client.Client, kymaName, kymaNamespace, } } -func createModuleTemplateSetsForKyma(modules []v1beta2.Module, modifiedVersion, channel string) error { - for _, module := range modules { - template := builder.NewModuleTemplateBuilder(). - WithNamespace(ControlPlaneNamespace). - WithModuleName(module.Name). - WithChannel(module.Channel). - WithOCM(compdescv2.SchemaVersion).Build() +func createModuleTemplateSetsForKyma(moduleName string, moduleVersion, channel string) error { + template := builder.NewModuleTemplateBuilder(). + WithName(moduleName + "-" + moduleVersion). + WithNamespace(ControlPlaneNamespace). + WithModuleName(moduleName). + WithVersion(moduleVersion). + Build() - descriptor, err := descriptorProvider.GetDescriptor(template) - if err != nil { - return err + if err := kcpClient.Create(ctx, template); err != nil { + return err + } + + mrm, err := GetModuleReleaseMeta(ctx, moduleName, ControlPlaneNamespace, kcpClient) + if err != nil && client.IgnoreNotFound(err) != nil { + return err + } + if mrm != nil { + // Ensure we don't duplicate channel entries + for _, ch := range mrm.Spec.Channels { + if ch.Channel == channel { + return fmt.Errorf("channel %q already exists in ModuleReleaseMeta %q"+ + " and is assigned version %q", channel, mrm.Name, ch.Version) + } } - descriptor.Version = modifiedVersion - newDescriptor, err := compdesc.Encode(descriptor.ComponentDescriptor, compdesc.DefaultJSONCodec) - if err != nil { + // Add new channel mapping + mrm.Spec.Channels = append(mrm.Spec.Channels, v1beta2.ChannelVersionAssignment{ + Channel: channel, + Version: moduleVersion, + }) + if err := kcpClient.Update(ctx, mrm); err != nil { return err } - template.Spec.Descriptor.Raw = newDescriptor - template.Spec.Channel = channel - template.Name = fmt.Sprintf("%s-%s", template.Name, channel) - if err := kcpClient.Create(ctx, template); err != nil { + } else { + mrm = ConfigureKCPModuleReleaseMeta(moduleName, channel, moduleVersion) + if err := kcpClient.Create(ctx, mrm); err != nil { return err } } - return nil + return registerDescriptor(mrm.Spec.OcmComponentName, moduleVersion) } diff --git a/tests/integration/controller/kyma/kyma_module_enable_test.go b/tests/integration/controller/kyma/kyma_module_enable_test.go index 2a350efb59..85670add2c 100644 --- a/tests/integration/controller/kyma/kyma_module_enable_test.go +++ b/tests/integration/controller/kyma/kyma_module_enable_test.go @@ -1,6 +1,8 @@ package kyma_test import ( + "context" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "sigs.k8s.io/controller-runtime/pkg/client" @@ -10,9 +12,14 @@ import ( . "github.com/kyma-project/lifecycle-manager/pkg/testutils" ) +const ( + ver110 = "1.1.0" +) + var _ = Describe("Given kyma CR with invalid module enabled", Ordered, func() { kyma := NewTestKyma("kyma") skrKyma := NewSKRKyma() + objTracker := &deletionTracker{} var skrClient client.Client var err error BeforeAll(func() { @@ -28,6 +35,12 @@ var _ = Describe("Given kyma CR with invalid module enabled", Ordered, func() { Eventually(DeleteCR, Timeout, Interval). WithContext(ctx). WithArguments(kcpClient, kyma).Should(Succeed()) + + // Clean up other resources created during the test + Eventually(objTracker.tryDeleteAll, Timeout, Interval). + WithContext(ctx). + WithArguments(kcpClient). + Should(Succeed()) }) BeforeEach(func() { Eventually(SyncKyma, Timeout, Interval). @@ -49,31 +62,37 @@ var _ = Describe("Given kyma CR with invalid module enabled", Ordered, 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(kcpClient, kyma, skrClient, skrKyma, module).Should(Succeed()) + WithContext(ctx). + WithArguments(kcpClient, kyma, skrClient, skrKyma, module, objTracker). + 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(kcpClient, kyma, skrClient, skrKyma, module).Should(Succeed()) + WithContext(ctx). + WithArguments(kcpClient, kyma, skrClient, skrKyma, module, objTracker). + Should(Succeed()) Eventually(expectKymaStatusModules(ctx, kyma, module.Name, shared.StateError), Timeout, Interval).Should(Succeed()) }) }) func givenKymaWithModule( + ctx context.Context, kcpClient client.Client, kcpKyma *v1beta2.Kyma, skrClient client.Client, remoteKyma *v1beta2.Kyma, module v1beta2.Module, + objTracker *deletionTracker, ) error { if err := EnableModule(ctx, skrClient, remoteKyma.GetName(), remoteKyma.GetNamespace(), module); err != nil { return err } Eventually(SyncKyma, Timeout, Interval). WithContext(ctx).WithArguments(kcpClient, kcpKyma).Should(Succeed()) - DeployModuleTemplates(ctx, kcpClient, kcpKyma) + DeployModuleTemplates(ctx, kcpClient, kcpKyma, ver110, objTracker) return nil } diff --git a/tests/integration/controller/kyma/kyma_moduleinfo_test.go b/tests/integration/controller/kyma/kyma_moduleinfo_test.go index 4d2b1d312c..f4219a70c1 100644 --- a/tests/integration/controller/kyma/kyma_moduleinfo_test.go +++ b/tests/integration/controller/kyma/kyma_moduleinfo_test.go @@ -7,6 +7,7 @@ import ( "github.com/kyma-project/lifecycle-manager/api/shared" "github.com/kyma-project/lifecycle-manager/api/v1beta2" + apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" . "github.com/kyma-project/lifecycle-manager/pkg/testutils" . "github.com/onsi/ginkgo/v2" @@ -15,6 +16,10 @@ import ( var ErrModuleNumberMismatch = errors.New("Spec.Modules number not match with Status.Modules") +const ( + ver123 = "1.2.3" +) + var ( kymaName = "kyma" moduleName = "module" @@ -27,11 +32,24 @@ var _ = Describe("Kyma module control", Ordered, func() { var skrClient client.Client var err error + objTracker := &deletionTracker{} BeforeAll(func() { - DeployModuleTemplates(ctx, kcpClient, &v1beta2.Kyma{Spec: v1beta2.KymaSpec{Modules: []v1beta2.Module{module}}}) Eventually(CreateCR, Timeout, Interval). WithContext(ctx). WithArguments(kcpClient, kyma).Should(Succeed()) + DeployModuleTemplates( + ctx, + kcpClient, + &v1beta2.Kyma{ + ObjectMeta: apimetav1.ObjectMeta{ + Namespace: shared.DefaultControlPlaneNamespace}, + Spec: v1beta2.KymaSpec{ + Modules: []v1beta2.Module{module}, + }, + }, + ver123, + objTracker, + ) Eventually(func() error { skrClient, err = testSkrContextFactory.Get(kyma.GetNamespacedName()) return err @@ -55,6 +73,10 @@ var _ = Describe("Kyma module control", Ordered, func() { Eventually(DeleteCR, Timeout, Interval). WithContext(ctx). WithArguments(kcpClient, kyma).Should(Succeed()) + Eventually(objTracker.tryDeleteAll, Timeout, Interval). + WithContext(ctx). + WithArguments(kcpClient). + Should(Succeed()) }) BeforeEach(func() { Eventually(SyncKyma, Timeout, Interval). @@ -83,7 +105,7 @@ var _ = Describe("Kyma module control", Ordered, func() { updateManifestStateWrapper(), modulesHaveReadyStatus), Entry("When remove module in spec, expect number of Manifests matches spec.modules", - removeModule(), + removeAllModules(), expectCorrectNumberOfModuleStatus), ) }) @@ -135,11 +157,12 @@ func modulesHaveReadyStatus(skrClient client.Client, kymaName string, kymaNamesp return nil } -func removeModule() func(client.Client, string, string) error { +func removeAllModules() func(client.Client, string, string) error { return func(skrClient client.Client, kymaName, kymaNamespace string) error { - createdKyma, err := GetKyma(ctx, skrClient, kymaName, kymaNamespace) - Expect(err).ShouldNot(HaveOccurred()) - createdKyma.Spec.Modules = []v1beta2.Module{} - return skrClient.Update(ctx, createdKyma) + updateFn := func(kyma *v1beta2.Kyma) error { + kyma.Spec.Modules = []v1beta2.Module{} + return nil + } + return UpdateKymaWithFunc(ctx, skrClient, kymaName, kymaNamespace, updateFn) } } diff --git a/tests/integration/controller/kyma/kyma_test.go b/tests/integration/controller/kyma/kyma_test.go index e80c8cde4a..57244b4204 100644 --- a/tests/integration/controller/kyma/kyma_test.go +++ b/tests/integration/controller/kyma/kyma_test.go @@ -6,7 +6,6 @@ import ( "fmt" apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - compdescv2 "ocm.software/ocm/api/ocm/compdesc/versions/v2" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/kyma-project/lifecycle-manager/api/shared" @@ -17,6 +16,11 @@ import ( . "github.com/onsi/gomega" . "github.com/kyma-project/lifecycle-manager/pkg/testutils" + "github.com/kyma-project/lifecycle-manager/pkg/testutils/random" +) + +const ( + ver111 = "1.1.1" ) var ( @@ -25,6 +29,9 @@ var ( ) var _ = Describe("Kyma enable Mandatory Module or non-existent Module Kyma.Spec.Modules", Ordered, func() { + mandatoryModuleName := "mandatory-" + random.Name() + objTracker := &deletionTracker{} + testCases := []struct { enableStatement string disableStatement string @@ -35,13 +42,13 @@ var _ = Describe("Kyma enable Mandatory Module or non-existent Module Kyma.Spec. enableStatement: "enabling one mandatory Module", disableStatement: "disabling mandatory Module", kymaName: "mandatory-module-kyma", - moduleName: "mandatory-template-operator", + moduleName: mandatoryModuleName, }, { enableStatement: "enabling one non-existing Module", disableStatement: "disabling non-existent Module", kymaName: "non-existing-module-kyma", - moduleName: "non-existent-module", + moduleName: "non-existing-" + random.Name(), }, } for _, testCase := range testCases { @@ -49,7 +56,6 @@ var _ = Describe("Kyma enable Mandatory Module or non-existent Module Kyma.Spec. skrKyma := NewSKRKyma() var skrClient client.Client var err error - BeforeAll(func() { Eventually(CreateCR, Timeout, Interval). WithContext(ctx). @@ -58,11 +64,20 @@ var _ = Describe("Kyma enable Mandatory Module or non-existent Module Kyma.Spec. skrClient, err = testSkrContextFactory.Get(kyma.GetNamespacedName()) return err }, Timeout, Interval).Should(Succeed()) + + // Deploy Mandatory ModuleTemplate so that "enabling one mandatory Module" test makes sense. + DeployMandatoryModuleTemplate(ctx, kcpClient, mandatoryModuleName, ver111, objTracker) }) AfterAll(func() { Eventually(DeleteCR, Timeout, Interval). WithContext(ctx). WithArguments(kcpClient, kyma).Should(Succeed()) + + // Clean up other resources created during the test + Eventually(objTracker.tryDeleteAll, Timeout, Interval). + WithContext(ctx). + WithArguments(kcpClient). + Should(Succeed()) }) BeforeEach(func() { @@ -72,11 +87,13 @@ var _ = Describe("Kyma enable Mandatory Module or non-existent Module Kyma.Spec. It("should result Kyma in Warning state", func() { By(testCase.enableStatement, func() { - skrKyma.Spec.Modules = append(skrKyma.Spec.Modules, v1beta2.Module{ + module := v1beta2.Module{ Name: testCase.moduleName, Managed: true, - }) - Eventually(skrClient.Update, Timeout, Interval). - WithContext(ctx).WithArguments(skrKyma).Should(Succeed()) + } + Eventually(EnableModule, Timeout, Interval). + WithContext(ctx). + WithArguments(skrClient, skrKyma.GetName(), skrKyma.GetNamespace(), module). + Should(Succeed()) }) By("checking the state to be Warning in KCP", func() { Eventually(KymaIsInState, Timeout, Interval). @@ -107,9 +124,14 @@ var _ = Describe("Kyma enable Mandatory Module or non-existent Module Kyma.Spec. }) It("should result Kyma in Ready state", func() { By(testCase.disableStatement, func() { - skrKyma.Spec.Modules = []v1beta2.Module{} - Eventually(skrClient.Update, Timeout, Interval). - WithContext(ctx).WithArguments(skrKyma).Should(Succeed()) + kymaUpdateFunc := func(skrKyma *v1beta2.Kyma) error { + skrKyma.Spec.Modules = []v1beta2.Module{} + return nil + } + Eventually(UpdateKymaWithFunc, Timeout, Interval). + WithContext(ctx). + WithArguments(skrClient, skrKyma.GetName(), skrKyma.GetNamespace(), kymaUpdateFunc). + Should(Succeed()) }) By("checking the state to be Ready in KCP", func() { Eventually(KymaIsInState, Timeout, Interval). @@ -144,26 +166,39 @@ var _ = Describe("Kyma enable Mandatory Module or non-existent Module Kyma.Spec. var _ = Describe("Kyma skip Reconciliation", Ordered, func() { kyma := NewTestKyma("kyma-test-update") module := NewTestModule("skr-module-update", v1beta2.DefaultChannel) + const moduleVersion = "0.0.1" kyma.Spec.Modules = append( kyma.Spec.Modules, module) - RegisterDefaultLifecycleForKymaWithoutTemplate(kyma) It("Should deploy ModuleTemplate", func() { data := builder.NewModuleCRBuilder().WithSpec(InitSpecKey, InitSpecValue).Build() template := builder.NewModuleTemplateBuilder(). + WithName(module.Name+"-"+moduleVersion). WithNamespace(ControlPlaneNamespace). WithModuleName(module.Name). - WithChannel(module.Channel). + WithVersion(moduleVersion). WithModuleCR(data). - WithOCM(compdescv2.SchemaVersion). - WithAnnotation(shared.IsClusterScopedAnnotation, shared.EnableLabelValue).Build() + WithAnnotation(shared.IsClusterScopedAnnotation, shared.EnableLabelValue). + Build() Eventually(kcpClient.Create, Timeout, Interval).WithContext(ctx). WithArguments(template). Should(Succeed()) }) + It("Should deploy ModuleReleaseMeta", func() { + moduleReleaseMeta := ConfigureKCPModuleReleaseMeta(module.Name, module.Channel, moduleVersion) + Eventually(kcpClient.Create, Timeout, Interval).WithContext(ctx). + WithArguments(moduleReleaseMeta). + Should(Succeed()) + + // descriptor is required to create Manifest + err := registerDescriptor(moduleReleaseMeta.Spec.OcmComponentName, moduleVersion) + Expect(err).ShouldNot(HaveOccurred()) + }) + It("Mark Kyma as skip Reconciliation", func() { + By("CR created", func() { for _, activeModule := range kyma.Spec.Modules { Eventually(ManifestExists, Timeout, Interval). @@ -226,6 +261,8 @@ var _ = Describe("Kyma.Spec.Status.Modules.Resource.Namespace should be empty fo kyma := NewTestKyma("kyma") skrKyma := NewSKRKyma() module := NewTestModule("test-module", v1beta2.DefaultChannel) + + const moduleVersion = "0.0.2" kyma.Spec.Modules = append( kyma.Spec.Modules, module) var skrClient client.Client @@ -247,21 +284,33 @@ 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(). + WithName(module.Name+"-"+moduleVersion). WithNamespace(ControlPlaneNamespace). WithModuleName(module.Name). - WithChannel(module.Channel). - WithOCM(compdescv2.SchemaVersion). - WithAnnotation(shared.IsClusterScopedAnnotation, shared.EnableLabelValue).Build() + WithVersion(moduleVersion). + WithAnnotation(shared.IsClusterScopedAnnotation, shared.EnableLabelValue). + Build() Eventually(kcpClient.Create, Timeout, Interval).WithContext(ctx). WithArguments(template). Should(Succeed()) } }) + It("Should deploy ModuleReleaseMeta", func() { + moduleReleaseMeta := ConfigureKCPModuleReleaseMeta(module.Name, module.Channel, moduleVersion) + Eventually(kcpClient.Create, Timeout, Interval).WithContext(ctx). + WithArguments(moduleReleaseMeta). + Should(Succeed()) + + // descriptor is required to create Manifest + err := registerDescriptor(moduleReleaseMeta.Spec.OcmComponentName, moduleVersion) + Expect(err).ShouldNot(HaveOccurred()) + }) + It("expect Kyma.Spec.Status.Modules.Resource.Namespace to be empty", func() { emptyNamespace := "" By("ensuring empty Module Status Resource Namespace in KCP") - Eventually(expectKymaModuleStatusWithNamespace). + Eventually(expectKymaModuleStatusWithNamespace, Timeout, Interval). WithContext(ctx). WithArguments(kcpClient, kyma, emptyNamespace). Should(Succeed()) diff --git a/tests/integration/controller/kyma/manifest_test.go b/tests/integration/controller/kyma/manifest_test.go index c08a1f73fb..e2bb3ab72e 100644 --- a/tests/integration/controller/kyma/manifest_test.go +++ b/tests/integration/controller/kyma/manifest_test.go @@ -15,8 +15,6 @@ import ( "ocm.software/ocm/api/ocm/cpi" "ocm.software/ocm/api/ocm/extensions/accessmethods/localblob" "ocm.software/ocm/api/ocm/extensions/repositories/genericocireg" - "ocm.software/ocm/api/ocm/extensions/repositories/genericocireg/componentmapping" - "sigs.k8s.io/controller-runtime/pkg/client" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -24,6 +22,7 @@ import ( "github.com/kyma-project/lifecycle-manager/api/shared" "github.com/kyma-project/lifecycle-manager/api/v1beta2" "github.com/kyma-project/lifecycle-manager/internal/descriptor/types" + "github.com/kyma-project/lifecycle-manager/internal/descriptor/types/ocmidentity" "github.com/kyma-project/lifecycle-manager/internal/pkg/flags" . "github.com/kyma-project/lifecycle-manager/pkg/testutils" ) @@ -65,12 +64,6 @@ var _ = Describe("Update Manifest CR", Ordered, func() { Should(Succeed()) By("Update Module Template spec.data") - moduleTemplateInCluster := &v1beta2.ModuleTemplate{} - err := kcpClient.Get(ctx, client.ObjectKey{ - Name: createModuleTemplateName(module), - Namespace: kyma.GetNamespace(), - }, moduleTemplateInCluster) - Expect(err).ToNot(HaveOccurred()) data := unstructured.Unstructured{} data.SetGroupVersionKind(schema.GroupVersionKind{ @@ -81,11 +74,17 @@ var _ = Describe("Update Manifest CR", Ordered, func() { data.Object["spec"] = map[string]interface{}{ "initKey": "valueUpdated", } - moduleTemplateInCluster.Spec.Data = &data - Eventually(kcpClient.Update, Timeout, Interval). + updateFn := func(moduleTemplateInCluster *v1beta2.ModuleTemplate) error { + moduleTemplateInCluster.Spec.Data = &data + return nil + } + mtName := v1beta2.CreateModuleTemplateName(module.Name, "1.0.1") + mtNamespace := kyma.GetNamespace() + + Eventually(UpdateModuleTemplateWithFunc, Timeout, Interval). WithContext(ctx). - WithArguments(moduleTemplateInCluster). + WithArguments(kcpClient, mtName, mtNamespace, updateFn). Should(Succeed()) By("CR updated with new value in spec.resource.spec") @@ -116,21 +115,27 @@ var _ = Describe("Manifest.Spec is rendered correctly", Ordered, func() { RegisterDefaultLifecycleForKyma(kyma) It("validate Manifest", func() { - moduleTemplate, err := GetModuleTemplate(ctx, kcpClient, module, kyma) + moduleTemplate, _, err := GetModuleTemplateInfo(ctx, kcpClient, module, kyma) Expect(err).NotTo(HaveOccurred()) - expectManifest := expectManifestFor(kyma) + expectManifest := expectManifestForFirstModule(kyma) By("checking Spec.Install") hasValidSpecInstall := func(manifest *v1beta2.Manifest) error { - moduleTemplateDescriptor, err := descriptorProvider.GetDescriptor(moduleTemplate) + ocmi, err := ocmidentity.NewComponentId(FullOCMName(module.Name), "1.0.1") + if err != nil { + return err + } + + moduleDescriptor, err := descriptorProvider.GetDescriptor(*ocmi) if err != nil { return err } return validateManifestSpecInstallSource(extractInstallImageSpec(manifest.Spec.Install), - moduleTemplateDescriptor) + moduleDescriptor) } + Eventually(expectManifest(hasValidSpecInstall), Timeout, Interval).Should(Succeed()) By("checking Spec.Resource") @@ -141,7 +146,11 @@ var _ = Describe("Manifest.Spec is rendered correctly", Ordered, func() { By("checking Spec.Version") hasValidSpecVersion := func(manifest *v1beta2.Manifest) error { - moduleTemplateDescriptor, err := descriptorProvider.GetDescriptor(moduleTemplate) + ocmi, err := ocmidentity.NewComponentId(FullOCMName(module.Name), "1.0.1") + if err != nil { + return err + } + moduleTemplateDescriptor, err := descriptorProvider.GetDescriptor(*ocmi) if err != nil { return err } @@ -182,18 +191,23 @@ var _ = Describe("Manifest.Spec is reset after manual update", Ordered, func() { manifest.Spec.Install.Source.Raw = updatedBytes err = kcpClient.Update(ctx, manifest) + Expect(err).ToNot(HaveOccurred()) }) It("validate Manifest", func() { - moduleTemplate, err := GetModuleTemplate(ctx, kcpClient, module, kyma) + moduleTemplate, _, err := GetModuleTemplateInfo(ctx, kcpClient, module, kyma) Expect(err).NotTo(HaveOccurred()) - expectManifest := expectManifestFor(kyma) + expectManifest := expectManifestForFirstModule(kyma) By("checking Spec.Install") hasValidSpecInstall := func(manifest *v1beta2.Manifest) error { - moduleTemplateDescriptor, err := descriptorProvider.GetDescriptor(moduleTemplate) + ocmi, err := ocmidentity.NewComponentId(FullOCMName(module.Name), "1.0.1") + if err != nil { + return err + } + moduleTemplateDescriptor, err := descriptorProvider.GetDescriptor(*ocmi) if err != nil { return err } @@ -360,7 +374,7 @@ func validateManifestSpecInstallSourceRepo(manifestImageSpec *v1beta2.ImageSpec, if concreteRepo.SubPath != "" { repositoryBaseURL = concreteRepo.Name() + "/" + concreteRepo.SubPath } - expectedSourceRepo := repositoryBaseURL + "/" + componentmapping.ComponentDescriptorNamespace + expectedSourceRepo := repositoryBaseURL if actualSourceRepo != expectedSourceRepo { return fmt.Errorf("Invalid SourceRepo: %s, expected: %s", actualSourceRepo, expectedSourceRepo) @@ -403,8 +417,8 @@ func validateManifestSpecResource(manifestResource, moduleTemplateData *unstruct return nil } -// expectManifest is a generic Manifest assertion function. -func expectManifestFor(kyma *v1beta2.Kyma) func(func(*v1beta2.Manifest) error) func() error { +// expectManifestForFirstModule is a generic Manifest assertion function. +func expectManifestForFirstModule(kyma *v1beta2.Kyma) func(func(*v1beta2.Manifest) error) func() error { return func(validationFn func(*v1beta2.Manifest) error) func() error { return func() error { // ensure manifest is refreshed each time the function is invoked diff --git a/tests/integration/controller/kyma/moduletemplate_install_test.go b/tests/integration/controller/kyma/moduletemplate_install_test.go index d9527133cc..4e863ccab5 100644 --- a/tests/integration/controller/kyma/moduletemplate_install_test.go +++ b/tests/integration/controller/kyma/moduletemplate_install_test.go @@ -3,7 +3,6 @@ package kyma_test import ( "fmt" - compdescv2 "ocm.software/ocm/api/ocm/compdesc/versions/v2" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/kyma-project/lifecycle-manager/api/shared" @@ -120,21 +119,11 @@ func givenKymaAndModuleTemplateCondition( isModuleTemplateBeta bool, ) func(client.Client, *v1beta2.Kyma) error { return func(skrClient client.Client, skrKyma *v1beta2.Kyma) error { - if skrKyma.Labels == nil { - skrKyma.Labels = map[string]string{} - } - if isKymaInternal { - skrKyma.Labels[shared.InternalLabel] = shared.EnableLabelValue - } - if isKymaBeta { - skrKyma.Labels[shared.BetaLabel] = shared.EnableLabelValue - } for _, module := range skrKyma.Spec.Modules { mtBuilder := builder.NewModuleTemplateBuilder(). WithNamespace(ControlPlaneNamespace). WithModuleName(module.Name). - WithChannel(module.Channel). - WithOCM(compdescv2.SchemaVersion) + WithChannel(module.Channel) if isModuleTemplateInternal { mtBuilder.WithLabel(shared.InternalLabel, shared.EnableLabelValue) } @@ -146,9 +135,26 @@ func givenKymaAndModuleTemplateCondition( WithArguments(template). Should(Succeed()) } - Eventually(skrClient.Update, Timeout, Interval). + + // wrap all the modifications to the skrKyma for later use + kymaUpdateFunc := func(kyma *v1beta2.Kyma) error { + if skrKyma.Labels == nil { + skrKyma.Labels = map[string]string{} + } + if isKymaInternal { + skrKyma.Labels[shared.InternalLabel] = shared.EnableLabelValue + } + if isKymaBeta { + skrKyma.Labels[shared.BetaLabel] = shared.EnableLabelValue + } + return nil + } + + Eventually(UpdateKymaWithFunc, Timeout, Interval). WithContext(ctx). - WithArguments(skrKyma).Should(Succeed()) + WithArguments(skrClient, skrKyma.GetName(), skrKyma.GetNamespace(), kymaUpdateFunc). + Should(Succeed()) + return nil } } diff --git a/tests/integration/controller/kyma/suite_test.go b/tests/integration/controller/kyma/suite_test.go index 88a8a3fe8b..03f1aa6d65 100644 --- a/tests/integration/controller/kyma/suite_test.go +++ b/tests/integration/controller/kyma/suite_test.go @@ -23,11 +23,13 @@ import ( "time" certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + "github.com/kyma-project/lifecycle-manager/pkg/testutils/builder" "go.uber.org/zap/zapcore" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" machineryaml "k8s.io/apimachinery/pkg/util/yaml" k8sclientscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" + compdescv2 "ocm.software/ocm/api/ocm/compdesc/versions/v2" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" ctrlruntime "sigs.k8s.io/controller-runtime/pkg/controller" @@ -45,6 +47,7 @@ import ( "github.com/kyma-project/lifecycle-manager/internal/pkg/flags" "github.com/kyma-project/lifecycle-manager/internal/pkg/metrics" "github.com/kyma-project/lifecycle-manager/internal/remote" + "github.com/kyma-project/lifecycle-manager/internal/service/componentdescriptor" "github.com/kyma-project/lifecycle-manager/internal/service/kyma/status/modules" "github.com/kyma-project/lifecycle-manager/internal/service/kyma/status/modules/generator" "github.com/kyma-project/lifecycle-manager/internal/service/kyma/status/modules/generator/fromerror" @@ -66,7 +69,12 @@ import ( // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. -const randomPort = "0" +const ( + randomPort = "0" + + // conforms to the url defined in `v1beta2_template_operator_current_ocm.yaml`. + staticOCIRegistryHost = "europe-west3-docker.pkg.dev/sap-kyma-jellyfish-dev/template-operator" +) var ( kcpClient client.Client @@ -77,6 +85,7 @@ var ( cfg *rest.Config descriptorProvider *provider.CachedDescriptorProvider testSkrContextFactory *testskrcontext.DualClusterFactory + registerDescriptor func(name, version string) error // register component descriptors for testing purposes. ) func TestAPIs(t *testing.T) { @@ -142,7 +151,14 @@ var _ = BeforeSuite(func() { Warning: 100 * time.Millisecond, } - descriptorProvider = provider.NewCachedDescriptorProvider() + fakeDescriptorService := &componentdescriptor.FakeService{} + descriptorProvider = provider.NewCachedDescriptorProvider(fakeDescriptorService) + compDescrawBytes := builder.ComponentDescriptorFactoryFromSchema(compdescv2.SchemaVersion) + registerDescriptor = func(name, version string) error { + fakeDescriptorService.RegisterWithNameVersionOverride(name, version, compDescrawBytes.Raw) + return nil + } + kcpClient = mgr.GetClient() testEventRec := event.NewRecorderWrapper(mgr.GetEventRecorderFor(shared.OperatorName)) testSkrContextFactory = testskrcontext.NewDualClusterFactory(kcpClient.Scheme(), testEventRec) @@ -168,6 +184,7 @@ var _ = BeforeSuite(func() { moduletemplateinfolookup.NewByChannelStrategy(kcpClient), moduletemplateinfolookup.NewByModuleReleaseMetaStrategy(kcpClient), })), + OCIRegistryHost: staticOCIRegistryHost, }).SetupWithManager(mgr, ctrlruntime.Options{}, kyma.SetupOptions{ListenerAddr: randomPort}) Expect(err).ToNot(HaveOccurred()) diff --git a/tests/integration/controller/mandatorymodule/deletion/controller_test.go b/tests/integration/controller/mandatorymodule/deletion/controller_test.go index fb9d343a9f..9474c0e40e 100644 --- a/tests/integration/controller/mandatorymodule/deletion/controller_test.go +++ b/tests/integration/controller/mandatorymodule/deletion/controller_test.go @@ -7,7 +7,6 @@ import ( "fmt" "path/filepath" - compdescv2 "ocm.software/ocm/api/ocm/compdesc/versions/v2" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -22,30 +21,30 @@ import ( ) const ( - mandatoryChannel = "dummychannel" - mandatoryModule = "mandatory-module" + mandatoryModuleName = "mandatory-module" + mandatoryModuleVersion = "1.0.1" ) var _ = Describe("Mandatory Module Deletion", Ordered, func() { Context("Given Kyma with one mandatory Module Manifest CR on Control-Plane", func() { kyma := NewTestKyma("no-module-kyma") - registerControlPlaneLifecycleForKyma(kyma) + registerControlPlaneLifecycleForKyma(kyma, mandatoryModuleName) It("Then Kyma CR should result in a ready state and mandatory manifest is created with IsMandatory label", func() { - Eventually(KymaIsInState). + Eventually(KymaIsInState, Timeout, Interval). WithContext(ctx). WithArguments(kyma.GetName(), kyma.GetNamespace(), kcpClient, shared.StateReady). Should(Succeed()) - Eventually(MandatoryManifestExistsWithLabelAndAnnotation). + Eventually(MandatoryManifestExistsWithLabelAndAnnotation, Timeout, Interval). WithContext(ctx). - WithArguments(kcpClient, shared.FQDN, DefaultFQDN). + WithArguments(kcpClient, shared.FQDN, FullOCMName(mandatoryModuleName)). Should(Succeed()) By("And mandatory finalizer is added to the mandatory ModuleTemplate", func() { - Eventually(mandatoryModuleTemplateFinalizerExists). + Eventually(mandatoryModuleTemplateFinalizerExists, Timeout, Interval). WithContext(ctx). WithArguments(kcpClient, client.ObjectKey{ Namespace: ControlPlaneNamespace, - Name: mandatoryModule, + Name: v1beta2.CreateModuleTemplateName(mandatoryModuleName, mandatoryModuleVersion), }). Should(Succeed()) }) @@ -53,22 +52,22 @@ var _ = Describe("Mandatory Module Deletion", Ordered, func() { }) It("When mandatory ModuleTemplate marked for deletion", func() { - Eventually(deleteMandatoryModuleTemplates). + Eventually(deleteMandatoryModule, Timeout, Interval). WithContext(ctx). WithArguments(kcpClient). Should(Succeed()) }) It("Then mandatory Manifest is deleted", func() { - Eventually(MandatoryManifestExistsWithLabelAndAnnotation). + Eventually(MandatoryManifestExistsWithLabelAndAnnotation, Timeout, Interval). WithContext(ctx). WithArguments(kcpClient, shared.FQDN, DefaultFQDN). Should(Not(Succeed())) By("And finalizer is removed from mandatory ModuleTemplate", func() { - Eventually(mandatoryModuleTemplateFinalizerExists). + Eventually(mandatoryModuleTemplateFinalizerExists, Timeout, Interval). WithContext(ctx). WithArguments(kcpClient, client.ObjectKey{ Namespace: ControlPlaneNamespace, - Name: mandatoryModule, + Name: mandatoryModuleName, }). Should(Not(Succeed())) }) @@ -76,59 +75,66 @@ var _ = Describe("Mandatory Module Deletion", Ordered, func() { }) }) -func registerControlPlaneLifecycleForKyma(kyma *v1beta2.Kyma) { +func registerControlPlaneLifecycleForKyma(kyma *v1beta2.Kyma, mandatoryModuleName string) { template := builder.NewModuleTemplateBuilder(). WithNamespace(ControlPlaneNamespace). - WithName(mandatoryModule). - WithModuleName(mandatoryModule). - WithChannel(mandatoryChannel). + WithName(v1beta2.CreateModuleTemplateName(mandatoryModuleName, mandatoryModuleVersion)). + WithModuleName(mandatoryModuleName). + WithLabel(shared.IsMandatoryModule, shared.EnableLabelValue). + WithVersion(mandatoryModuleVersion). WithMandatory(true). - WithOCM(compdescv2.SchemaVersion). - WithLabel(shared.IsMandatoryModule, shared.EnableLabelValue).Build() + Build() + moduleReleaseMeta := ConfigureKCPMandatoryModuleReleaseMeta(template.Spec.ModuleName, template.Spec.Version) + mandatoryManifest := NewTestManifest("mandatory-module") - mandatoryManifest.Spec.Version = "1.1.1-e2e-test" mandatoryManifest.Labels[shared.IsMandatoryModule] = "true" + mandatoryManifest.Annotations = map[string]string{shared.FQDN: moduleReleaseMeta.Spec.OcmComponentName} + mandatoryManifest.Spec.Version = mandatoryModuleVersion BeforeAll(func() { - Eventually(CreateCR). + err := registerDescriptor(moduleReleaseMeta.Spec.OcmComponentName, template.Spec.Version) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(CreateCR, Timeout, Interval). WithContext(ctx). WithArguments(kcpClient, template).Should(Succeed()) + Eventually(CreateCR, Timeout, Interval). + WithContext(ctx). + WithArguments(kcpClient, moduleReleaseMeta).Should(Succeed()) // Set labels and state manual, since we do not start the Kyma Controller kyma.Labels[shared.ManagedBy] = shared.OperatorName - Eventually(CreateCR). + Eventually(CreateCR, Timeout, Interval). WithContext(ctx). WithArguments(kcpClient, kyma).Should(Succeed()) - Eventually(SetKymaState). + Eventually(SetKymaState, Timeout, Interval). WithContext(ctx). WithArguments(kyma, reconciler, shared.StateReady).Should(Succeed()) installName := filepath.Join("main-dir", "installs") - mandatoryManifest.Annotations = map[string]string{shared.FQDN: DefaultFQDN} validImageSpec, err := CreateOCIImageSpecFromFile(installName, server.Listener.Addr().String(), manifestFilePath) Expect(err).NotTo(HaveOccurred()) imageSpecByte, err := json.Marshal(validImageSpec) Expect(err).NotTo(HaveOccurred()) - Eventually(InstallManifest). + Eventually(InstallManifest, Timeout, Interval). WithContext(ctx). WithArguments(kcpClient, mandatoryManifest, imageSpecByte, false). Should(Succeed()) }) AfterAll(func() { - Eventually(DeleteCR). + Eventually(DeleteCR, Timeout, Interval). WithContext(ctx). WithArguments(kcpClient, kyma).Should(Succeed()) }) BeforeEach(func() { By("get latest kyma CR") - Eventually(SyncKyma). + Eventually(SyncKyma, Timeout, Interval). WithContext(ctx).WithArguments(kcpClient, kyma).Should(Succeed()) }) } -func deleteMandatoryModuleTemplates(ctx context.Context, clnt client.Client) error { +func deleteMandatoryModule(ctx context.Context, clnt client.Client) error { templates := v1beta2.ModuleTemplateList{} if err := clnt.List(ctx, &templates); err != nil { return fmt.Errorf("failed to list ModuleTemplates: %w", err) @@ -139,6 +145,17 @@ func deleteMandatoryModuleTemplates(ctx context.Context, clnt client.Client) err if err := clnt.Delete(ctx, &template); err != nil { return fmt.Errorf("failed to delete ModuleTemplate: %w", err) } + moduleReleaseMeta := v1beta2.ModuleReleaseMeta{} + err := clnt.Get(ctx, client.ObjectKey{ + Namespace: template.Namespace, + Name: template.Spec.ModuleName, + }, &moduleReleaseMeta) + if err != nil && !errors.Is(err, client.IgnoreNotFound(err)) { + return fmt.Errorf("failed to get ModuleReleaseMeta: %w", err) + } + if err := clnt.Delete(ctx, &moduleReleaseMeta); err != nil { + return fmt.Errorf("failed to delete ModuleReleaseMeta: %w", err) + } } } @@ -156,3 +173,12 @@ func mandatoryModuleTemplateFinalizerExists(ctx context.Context, clnt client.Cli } return errors.New("ModuleTemplate does not contain mandatory finalizer") } + +func ConfigureKCPMandatoryModuleReleaseMeta(moduleName, moduleVersion string) *v1beta2.ModuleReleaseMeta { + return builder.NewModuleReleaseMetaBuilder(). + WithNamespace(ControlPlaneNamespace). + WithModuleName(moduleName). + WithOcmComponentName(FullOCMName(moduleName)). + WithMandatory(moduleVersion). + Build() +} diff --git a/tests/integration/controller/mandatorymodule/deletion/suite_test.go b/tests/integration/controller/mandatorymodule/deletion/suite_test.go index e2984bda74..670d1c624a 100644 --- a/tests/integration/controller/mandatorymodule/deletion/suite_test.go +++ b/tests/integration/controller/mandatorymodule/deletion/suite_test.go @@ -36,6 +36,10 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "github.com/kyma-project/lifecycle-manager/internal/service/componentdescriptor" + "github.com/kyma-project/lifecycle-manager/pkg/testutils/builder" + compdescv2 "ocm.software/ocm/api/ocm/compdesc/versions/v2" + "github.com/kyma-project/lifecycle-manager/api" "github.com/kyma-project/lifecycle-manager/api/shared" "github.com/kyma-project/lifecycle-manager/internal/controller/mandatorymodule" @@ -67,6 +71,8 @@ var ( cancel context.CancelFunc manifestFilePath string server *httptest.Server + + registerDescriptor func(name, version string) error // register component descriptors for testing purposes. ) func TestAPIs(t *testing.T) { @@ -123,7 +129,14 @@ var _ = BeforeSuite(func() { Warning: 100 * time.Millisecond, } - descriptorProvider := provider.NewCachedDescriptorProvider() + fakeDescriptorService := &componentdescriptor.FakeService{} + descriptorProvider := provider.NewCachedDescriptorProvider(fakeDescriptorService) + compDescrawBytes := builder.ComponentDescriptorFactoryFromSchema(compdescv2.SchemaVersion) + registerDescriptor = func(name, version string) error { + fakeDescriptorService.RegisterWithNameVersionOverride(name, version, compDescrawBytes.Raw) + return nil + } + reconciler = &mandatorymodule.DeletionReconciler{ Client: mgr.GetClient(), Event: event.NewRecorderWrapper(mgr.GetEventRecorderFor(shared.OperatorName)), diff --git a/tests/integration/controller/mandatorymodule/installation/controller_test.go b/tests/integration/controller/mandatorymodule/installation/controller_test.go index add0758e7e..6f9715accd 100644 --- a/tests/integration/controller/mandatorymodule/installation/controller_test.go +++ b/tests/integration/controller/mandatorymodule/installation/controller_test.go @@ -5,7 +5,6 @@ import ( "errors" k8slabels "k8s.io/apimachinery/pkg/labels" - compdescv2 "ocm.software/ocm/api/ocm/compdesc/versions/v2" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/kyma-project/lifecycle-manager/api/shared" @@ -24,16 +23,16 @@ var ( ) const ( - mandatoryChannel = "dummychannel" + mandatoryModuleName = "mandatory-module" ) var _ = Describe("Mandatory Module Installation", Ordered, func() { Context("Given Kyma with no Module and one mandatory ModuleTemplate on Control-Plane", func() { kyma := NewTestKyma("no-module-kyma") - registerControlPlaneLifecycleForKyma(kyma) + registerControlPlaneLifecycleForKyma(kyma, mandatoryModuleName) It("Then Kyma CR should result in a ready state immediately as there are no modules", func() { - Eventually(KymaIsInState). + Eventually(KymaIsInState, Timeout, Interval). WithContext(ctx). WithArguments(kyma.GetName(), kyma.GetNamespace(), kcpClient, shared.StateReady). Should(Succeed()) @@ -49,14 +48,14 @@ var _ = Describe("Mandatory Module Installation", Ordered, func() { return ErrWrongModulesStatus } return nil - }). + }, Timeout, Interval). Should(Succeed()) }) It("And Manifest CR for the Mandatory Module should be created with correct Owner Reference", func() { - Eventually(checkMandatoryManifestForKyma). + Eventually(checkMandatoryManifestForKyma, Timeout, Interval). WithContext(ctx). - WithArguments(kyma, DefaultFQDN). + WithArguments(kyma, FullOCMName(mandatoryModuleName)). Should(Succeed()) }) }) @@ -66,10 +65,10 @@ var _ = Describe("Skipping Mandatory Module Installation", Ordered, func() { Context("Given Kyma with no Module and one mandatory ModuleTemplate on Control-Plane", func() { kyma := NewTestKyma("skip-reconciliation-kyma") kyma.Labels[shared.SkipReconcileLabel] = "true" - registerControlPlaneLifecycleForKyma(kyma) + registerControlPlaneLifecycleForKyma(kyma, mandatoryModuleName) It("When Kyma has 'skip-reconciliation' label, then no Mandatory Module Manifest should be created", func() { - Eventually(checkMandatoryManifestForKyma). + Eventually(checkMandatoryManifestForKyma, Timeout, Interval). WithContext(ctx). WithArguments(kyma, DefaultFQDN). Should(Equal(ErrNoMandatoryManifest)) @@ -77,41 +76,51 @@ var _ = Describe("Skipping Mandatory Module Installation", Ordered, func() { }) }) -func registerControlPlaneLifecycleForKyma(kyma *v1beta2.Kyma) { +func registerControlPlaneLifecycleForKyma(kyma *v1beta2.Kyma, mandatoryModuleName string) { + const version = "1.0.0" template := builder.NewModuleTemplateBuilder(). WithNamespace(ControlPlaneNamespace). - WithModuleName("mandatory-module"). + WithModuleName(mandatoryModuleName). WithLabel(shared.IsMandatoryModule, shared.EnableLabelValue). - WithChannel(mandatoryChannel). + WithVersion(version). WithMandatory(true). - WithOCM(compdescv2.SchemaVersion).Build() + Build() + moduleReleaseMeta := ConfigureKCPMandatoryModuleReleaseMeta(template.Spec.ModuleName, template.Spec.Version) BeforeAll(func() { - Eventually(CreateCR). + err := registerDescriptor(moduleReleaseMeta.Spec.OcmComponentName, template.Spec.Version) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(CreateCR, Timeout, Interval). WithContext(ctx). WithArguments(kcpClient, template).Should(Succeed()) + Eventually(CreateCR, Timeout, Interval). + WithContext(ctx). + WithArguments(kcpClient, moduleReleaseMeta).Should(Succeed()) // Set labels and state manual, since we do not start the Kyma Controller kyma.Labels[shared.ManagedBy] = shared.OperatorName - Eventually(CreateCR). + Eventually(CreateCR, Timeout, Interval). WithContext(ctx). WithArguments(kcpClient, kyma).Should(Succeed()) - Eventually(SetKymaState). + Eventually(SetKymaState, Timeout, Interval). WithContext(ctx). WithArguments(kyma, reconciler, shared.StateReady).Should(Succeed()) }) AfterAll(func() { - Eventually(DeleteCR). + Eventually(DeleteCR, Timeout, Interval). WithContext(ctx). WithArguments(kcpClient, kyma).Should(Succeed()) - Eventually(DeleteCR). + Eventually(DeleteCR, Timeout, Interval). + WithContext(ctx). + WithArguments(kcpClient, moduleReleaseMeta).Should(Succeed()) + Eventually(DeleteCR, Timeout, Interval). WithContext(ctx). WithArguments(kcpClient, template).Should(Succeed()) }) BeforeEach(func() { By("get latest kyma CR") - Eventually(SyncKyma). + Eventually(SyncKyma, Timeout, Interval). WithContext(ctx).WithArguments(kcpClient, kyma).Should(Succeed()) }) } @@ -131,3 +140,12 @@ func checkMandatoryManifestForKyma(ctx context.Context, kyma *v1beta2.Kyma, fqdn } return ErrNoMandatoryManifest } + +func ConfigureKCPMandatoryModuleReleaseMeta(moduleName, moduleVersion string) *v1beta2.ModuleReleaseMeta { + return builder.NewModuleReleaseMetaBuilder(). + WithNamespace(ControlPlaneNamespace). + WithModuleName(moduleName). + WithOcmComponentName(FullOCMName(moduleName)). + WithMandatory(moduleVersion). + Build() +} diff --git a/tests/integration/controller/mandatorymodule/installation/suite_test.go b/tests/integration/controller/mandatorymodule/installation/suite_test.go index 615cd286c0..faefc21f15 100644 --- a/tests/integration/controller/mandatorymodule/installation/suite_test.go +++ b/tests/integration/controller/mandatorymodule/installation/suite_test.go @@ -22,6 +22,9 @@ import ( "testing" "time" + "github.com/kyma-project/lifecycle-manager/pkg/testutils/builder" + compdescv2 "ocm.software/ocm/api/ocm/compdesc/versions/v2" + certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" "go.uber.org/zap/zapcore" apicorev1 "k8s.io/api/core/v1" @@ -39,6 +42,7 @@ import ( "github.com/kyma-project/lifecycle-manager/internal/descriptor/provider" "github.com/kyma-project/lifecycle-manager/internal/pkg/flags" "github.com/kyma-project/lifecycle-manager/internal/pkg/metrics" + "github.com/kyma-project/lifecycle-manager/internal/service/componentdescriptor" "github.com/kyma-project/lifecycle-manager/internal/setup" "github.com/kyma-project/lifecycle-manager/pkg/log" "github.com/kyma-project/lifecycle-manager/pkg/queue" @@ -58,11 +62,12 @@ const ( ) var ( - reconciler *mandatorymodule.InstallationReconciler - kcpClient client.Client - singleClusterEnv *envtest.Environment - ctx context.Context - cancel context.CancelFunc + reconciler *mandatorymodule.InstallationReconciler + kcpClient client.Client + singleClusterEnv *envtest.Environment + ctx context.Context + cancel context.CancelFunc + registerDescriptor func(name, version string) error // register component descriptors for the testing purposes ) func TestAPIs(t *testing.T) { @@ -114,7 +119,14 @@ var _ = BeforeSuite(func() { Warning: 100 * time.Millisecond, } - descriptorProvider := provider.NewCachedDescriptorProvider() + fakeDescriptorService := &componentdescriptor.FakeService{} + descriptorProvider := provider.NewCachedDescriptorProvider(fakeDescriptorService) + compDescrawBytes := builder.ComponentDescriptorFactoryFromSchema(compdescv2.SchemaVersion) + registerDescriptor = func(name, version string) error { + fakeDescriptorService.RegisterWithNameVersionOverride(name, version, compDescrawBytes.Raw) + return nil + } + reconciler = &mandatorymodule.InstallationReconciler{ Client: mgr.GetClient(), DescriptorProvider: descriptorProvider, diff --git a/tests/integration/controller/moduletemplate/moduletemplate_test.go b/tests/integration/controller/moduletemplate/moduletemplate_test.go index e8e313cfc0..3a2b0fe26a 100644 --- a/tests/integration/controller/moduletemplate/moduletemplate_test.go +++ b/tests/integration/controller/moduletemplate/moduletemplate_test.go @@ -3,8 +3,6 @@ package moduletemplate_test import ( "fmt" - compdescv2 "ocm.software/ocm/api/ocm/compdesc/versions/v2" - "github.com/kyma-project/lifecycle-manager/api/v1beta2" "github.com/kyma-project/lifecycle-manager/pkg/testutils/builder" @@ -33,7 +31,7 @@ var _ = Describe("ModuleTemplate version is not empty", Ordered, func() { WithVersion(givenVersion). WithModuleName(""). WithChannel(module.Channel). - WithOCM(compdescv2.SchemaVersion).Build() + Build() err := kcpClient.Create(ctx, template) if shouldSucceed { @@ -128,7 +126,7 @@ var _ = Describe("ModuleTemplate version is not empty", Ordered, func() { WithModuleName(givenModuleName). WithVersion(""). WithChannel(module.Channel). - WithOCM(compdescv2.SchemaVersion).Build() + Build() err := kcpClient.Create(ctx, template) if shouldSucceed { diff --git a/tests/integration/controller/moduletemplate/suite_test.go b/tests/integration/controller/moduletemplate/suite_test.go index a9cc1ab33e..83efd19d48 100644 --- a/tests/integration/controller/moduletemplate/suite_test.go +++ b/tests/integration/controller/moduletemplate/suite_test.go @@ -32,7 +32,6 @@ import ( metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "github.com/kyma-project/lifecycle-manager/api" - "github.com/kyma-project/lifecycle-manager/internal/descriptor/provider" "github.com/kyma-project/lifecycle-manager/internal/setup" "github.com/kyma-project/lifecycle-manager/pkg/log" "github.com/kyma-project/lifecycle-manager/tests/integration" @@ -51,12 +50,11 @@ import ( const randomPort = "0" var ( - kcpClient client.Client - mgr manager.Manager - controlPlaneEnv *envtest.Environment - ctx context.Context - cancel context.CancelFunc - descriptorProvider *provider.CachedDescriptorProvider + kcpClient client.Client + mgr manager.Manager + controlPlaneEnv *envtest.Environment + ctx context.Context + cancel context.CancelFunc ) func TestAPIs(t *testing.T) { @@ -100,7 +98,6 @@ var _ = BeforeSuite(func() { }) Expect(err).ToNot(HaveOccurred()) - descriptorProvider = provider.NewCachedDescriptorProvider() kcpClient = mgr.GetClient() Eventually(CreateNamespace, Timeout, Interval). WithContext(ctx). diff --git a/tests/integration/controller/withwatcher/suite_test.go b/tests/integration/controller/withwatcher/suite_test.go index 4a8aeda10b..4744d42ebe 100644 --- a/tests/integration/controller/withwatcher/suite_test.go +++ b/tests/integration/controller/withwatcher/suite_test.go @@ -45,7 +45,6 @@ import ( "github.com/kyma-project/lifecycle-manager/api/shared" "github.com/kyma-project/lifecycle-manager/internal/controller/kyma" watcherctrl "github.com/kyma-project/lifecycle-manager/internal/controller/watcher" - "github.com/kyma-project/lifecycle-manager/internal/descriptor/provider" "github.com/kyma-project/lifecycle-manager/internal/event" "github.com/kyma-project/lifecycle-manager/internal/pkg/flags" "github.com/kyma-project/lifecycle-manager/internal/pkg/metrics" @@ -240,7 +239,7 @@ var _ = BeforeSuite(func() { Event: testEventRec, RequeueIntervals: intervals, SKRWebhookManager: skrWebhookChartManager, - DescriptorProvider: provider.NewCachedDescriptorProvider(), + DescriptorProvider: nil, // no descriptor provider needed for these tests SyncRemoteCrds: remote.NewSyncCrdsUseCase(kcpClient, testSkrContextFactory, nil), ModulesStatusHandler: modules.NewStatusHandler(moduleStatusGen, kcpClient, noOpMetricsFunc), RemoteSyncNamespace: flags.DefaultRemoteSyncNamespace, diff --git a/unit-test-coverage-lifecycle-manager.yaml b/unit-test-coverage-lifecycle-manager.yaml index 3fa5e36a79..fbc08d3e66 100644 --- a/unit-test-coverage-lifecycle-manager.yaml +++ b/unit-test-coverage-lifecycle-manager.yaml @@ -2,8 +2,8 @@ packages: internal/controller: 100 internal/controller/istiogatewaysecret: 28 internal/crd: 92 - internal/descriptor/cache: 88 - internal/descriptor/provider: 66 + internal/descriptor/cache: 90 + internal/descriptor/provider: 100 internal/event: 100 internal/gatewaysecret/cabundle: 100 internal/gatewaysecret/legacy: 100 @@ -23,6 +23,7 @@ packages: internal/remote: 30 internal/repository/kyma: 100 + internal/repository/oci: 94 internal/repository/watcher/certificate: 100 internal/repository/watcher/certificate/certmanager/certificate: 100 internal/repository/watcher/certificate/gcm/certificate: 98 @@ -32,6 +33,7 @@ packages: internal/repository/moduletemplate: 100 internal/repository/modulereleasemeta: 100 + internal/service/componentdescriptor: 63 internal/service/kyma/status/modules/generator: 100 internal/service/kyma/status/modules/generator/fromerror: 100 internal/service/manifest/orphan: 100