├── pkg ├── test_samples │ ├── empty-file.yaml │ ├── oci │ │ ├── invalid-yaml.yaml │ │ ├── rendered.tar │ │ ├── valid-yaml.yaml │ │ └── manifest-contains-duplicate-resources.yaml │ └── auth_secret.yaml ├── common │ └── error.go ├── log │ ├── levels.go │ └── logger.go ├── templatelookup │ ├── common │ │ └── errors.go │ ├── moduletemplateinfolookup │ │ ├── common_test.go │ │ └── common.go │ └── moduletemplateinfo_test.go ├── testutils │ ├── ocm.go │ ├── namespace.go │ ├── random │ │ └── random.go │ └── networkpolicy.go ├── status │ └── conditions.go ├── util │ └── error_test.go └── matcher │ └── matcher_test.go ├── internal ├── maintenancewindows │ └── testdata │ │ ├── invalid-policy.json │ │ └── policy.json ├── errors │ ├── errors.go │ ├── kyma │ │ └── deletion │ │ │ └── errors.go │ └── mandatorymodule │ │ ├── deletion │ │ └── errors.go │ │ └── installation │ │ └── errors.go ├── result │ ├── result.go │ └── kyma │ │ └── usecase │ │ └── use_cases.go ├── service │ ├── watcher │ │ ├── certificate │ │ │ ├── name │ │ │ │ └── name.go │ │ │ └── secret │ │ │ │ └── data │ │ │ │ └── data.go │ │ └── resources │ │ │ └── skr_secret.go │ ├── mandatorymodule │ │ └── deletion │ │ │ ├── usecases │ │ │ ├── event.go │ │ │ ├── event_test.go │ │ │ ├── skip_non_deleting.go │ │ │ └── remove_finalizer.go │ │ │ └── deletion_service.go │ ├── kyma │ │ └── deletion │ │ │ └── usecases │ │ │ ├── types.go │ │ │ ├── types_test.go │ │ │ ├── delete_metrics.go │ │ │ ├── delete_manifests.go │ │ │ ├── drop_kyma_finalizer.go │ │ │ ├── set_status_deleting.go │ │ │ └── delete_skr_kyma.go │ └── skrclient │ │ └── cache │ │ ├── client_cache_test.go │ │ └── client_cache.go ├── watch │ └── change_handler_client.go ├── common │ ├── fieldowners │ │ └── const.go │ ├── fieldindex │ │ └── indexed_fields.go │ └── errors.go ├── resources.go ├── repository │ ├── watcher │ │ └── certificate │ │ │ ├── config │ │ │ └── config.go │ │ │ ├── errors │ │ │ └── error.go │ │ │ ├── common_test.go │ │ │ ├── common.go │ │ │ └── gcm │ │ │ └── renewal │ │ │ └── certificate_repo.go │ ├── skr │ │ ├── crd │ │ │ └── repo_test.go │ │ └── kyma │ │ │ ├── repo_test.go │ │ │ └── status │ │ │ └── repo_test.go │ └── istiogateway │ │ └── istio_gateway.go ├── declarative │ └── v2 │ │ ├── spec.go │ │ ├── object.go │ │ ├── resource_test.go │ │ └── state_check.go ├── descriptor │ ├── cache │ │ ├── descriptorkey.go │ │ ├── key_test.go │ │ └── cache.go │ └── types │ │ ├── ocmidentity │ │ └── ocmidentity.go │ │ ├── descriptor.go │ │ └── descriptor_test.go ├── controller │ ├── README.md │ ├── watcherEventAdapter.go │ ├── purge │ │ └── setup.go │ └── watcher │ │ └── setup.go ├── manifest │ ├── keychainprovider │ │ └── default.go │ ├── filemutex │ │ └── cache.go │ ├── status │ │ ├── state.go │ │ └── init.go │ └── img │ │ └── layer.go ├── rate_limiter.go ├── pkg │ └── metrics │ │ ├── shared.go │ │ ├── maintance_window.go │ │ ├── godebug_internal_test.go │ │ └── watcher.go ├── util │ └── collections │ │ ├── collections.go │ │ ├── filter.go │ │ └── diffcalc.go ├── gatewaysecret │ └── handler.go ├── crd │ └── cache.go ├── istio │ └── errors.go ├── tests │ └── api │ │ └── label_annotations_test.go ├── event │ └── event.go ├── util.go └── remote │ └── client_cache.go ├── maintenancewindows ├── resolver │ ├── testdata │ │ ├── ruleset-2.json │ │ └── ruleset-3.yaml │ └── resolver_test.go ├── go.mod ├── README.md ├── Makefile └── go.sum ├── unit-test-coverage-api.yaml ├── unit-test-coverage-maintenancewindows.yaml ├── .vscode └── extensions.json ├── config ├── watcher_local_test │ ├── namespace.yaml │ └── patches │ │ ├── fips_only_mode.yaml │ │ ├── deployment_resources.yaml │ │ └── unique_deployment_webhook_patch.yaml ├── rbac │ ├── service_account.yaml │ ├── kustomization.yaml │ ├── crd_cluster_role_binding.yaml │ ├── manager_role_binding.yaml │ ├── leader_election_role_binding.yaml │ ├── crd_cluster_role.yaml │ └── leader_election_role.yaml ├── watcher_local_test_gcm │ ├── namespace.yaml │ └── patches │ │ ├── fips_only_mode.yaml │ │ ├── deployment_resources.yaml │ │ └── unique_deployment_webhook_patch.yaml ├── samples │ ├── .gitignore │ ├── component-integration-installed │ │ ├── operator_v1beta2_kyma.yaml │ │ └── v1_secret_remote_cluster_access.yaml │ └── tests │ │ └── istio-test-resources.yaml ├── istio │ ├── patches │ │ ├── deployment_istio_inject.yaml │ │ └── deployment_exclude_webhook_port.yaml │ ├── commonlabels_override.yaml │ ├── kustomization.yaml │ └── endpoints_authorization_policy.yaml ├── webhook │ ├── service.yaml │ ├── kustomization.yaml │ └── kustomizeconfig.yaml ├── certmanager │ ├── patches │ │ ├── cainjection_in_kymas.yaml │ │ ├── cainjection_in_manifests.yaml │ │ ├── cainjection_in_watchers.yaml │ │ └── cainjection_in_moduletemplates.yaml │ ├── certmanager_role_binding.yaml │ ├── certmanager_role.yaml │ └── certificate_webhook.yaml ├── manager │ ├── kustomization.yaml │ └── metrics_service.yaml ├── control-plane │ └── patches │ │ ├── deployment_resources.yaml │ │ └── unique_deployment_webhook_patch.yaml ├── gardener-certmanager │ ├── gardener_certmanager_role_binding.yaml │ ├── kustomizeconfig.yaml │ ├── gardener_certmanager_role.yaml │ ├── kustomization.yaml │ ├── certificate_webhook.yaml │ └── certificate_watcher.yaml ├── crd │ ├── kustomization.yaml │ └── kustomizeconfig.yaml ├── grafana │ └── kustomization.yaml ├── maintenance_windows │ ├── patches │ │ └── volume_mount.yaml │ └── kustomization.yaml └── watcher │ ├── kustomization.yaml │ ├── gateway.yaml │ └── kyma_watcher.yaml ├── api ├── shared │ ├── external_labels.go │ ├── namespace.go │ ├── external_annotations.go │ ├── cr_finalizer.go │ ├── last_operation.go │ ├── istio.go │ ├── channel.go │ ├── cr_kind.go │ ├── cr_kind_test.go │ ├── operator_annotations.go │ └── resource.go ├── README.md ├── v1beta1 │ ├── last_operation.go │ └── resource.go ├── Makefile ├── scheme.go └── v1beta2 │ └── manifest_types_test.go ├── .dockerignore ├── scripts └── tests │ ├── clusters_cleanup.sh │ ├── undeploy_kyma.sh │ ├── install_crds.sh │ ├── apply_cm_certificate_management.sh │ ├── ocm-config-local-registry.yaml │ ├── remove_skr_host_from_coredns.sh │ ├── e2e.sh │ ├── apply_gcm_certificate_management.sh │ ├── deploy_mandatory_modulereleasemeta.sh │ ├── ocm-config-private-registry.yaml │ ├── deploy_klm_from_sources.sh │ ├── add_skr_host_to_coredns.sh │ ├── deploy_modulereleasemeta.sh │ ├── https_server.go │ └── deploy_klm_from_registry.sh ├── CONTRIBUTING.md ├── .mlc.config.json ├── docs ├── user │ ├── README.md │ ├── 03-skipping-maintenance-windows.md │ └── 01-10-kyma-crd.md ├── contributor │ ├── resources │ │ └── 04-watcher.md │ ├── README.md │ └── adr │ │ ├── 005-consistent-naming.md │ │ └── 000-document-decisions-as-adrs.md ├── README.md └── operator │ └── README.md ├── tests ├── e2e │ ├── module_status_decoupling_with_deployment_test.go │ ├── module_status_decoupling_with_statefulset_test.go │ ├── kyma_deprovision_background_test.go │ ├── kyma_deprovision_foreground_test.go │ ├── README.md │ └── commontestutils │ │ └── statefulset.go └── integration │ ├── paths.go │ ├── controller │ ├── kcp │ │ └── controller_test.go │ └── kyma │ │ └── kyma_module_version_test.go │ └── commontestutils │ └── skrcontextimpl │ └── accessmanager.go ├── .mockery.yaml ├── hack ├── k3d-secret-gen.sh └── boilerplate.go.txt ├── .github ├── actions │ ├── patch-skr-hostname-in-kcp │ │ └── action.yml │ ├── switch-kubectl-context │ │ └── action.yml │ ├── setup-https-server │ │ └── action.yml │ ├── install-yq │ │ └── action.yml │ ├── install-k3d │ │ └── action.yml │ ├── install-istioctl │ │ └── action.yml │ ├── export-kubeconfigs │ │ └── action.yml │ ├── install-modulectl │ │ └── action.yml │ ├── setup-test-clusters │ │ └── action.yml │ ├── setup-test-clusters-gcm │ │ └── action.yml │ ├── wait-for-image-build │ │ └── action.yml │ └── setup-private-registry │ │ └── action.yml ├── scripts │ └── release │ │ ├── validate_release_tag.sh │ │ ├── publish_release.sh │ │ ├── wait_for_image.sh │ │ ├── get_release_by_tag.sh │ │ ├── draft_release.sh │ │ └── create_changelog.sh ├── workflows │ ├── lint-yaml.yml │ ├── create-release-api.yml │ ├── test-unit.yml │ ├── create-release-maintenancewindows.yml │ ├── lint-markdown-links.yml │ ├── lint-conventional-prs.yml │ ├── report-sprint-commits.yml │ └── report-acceptance-criteria.yml ├── pull-request-template.md ├── ISSUE_TEMPLATE │ ├── documentation-improvement.md │ └── epic.md ├── dependabot.yml └── DISCUSSION_TEMPLATE │ └── q-a.yml ├── CODE_OF_CONDUCT.md ├── cmd └── composition │ ├── provider │ └── componentdescriptorcache │ │ └── provider.go │ ├── service │ ├── componentdescriptor │ │ └── componentdescriptor.go │ └── mandatorymodule │ │ ├── deletion │ │ └── deletion_service.go │ │ └── installation │ │ └── installation_service.go │ └── repository │ └── oci │ └── ocirepository.go ├── sec-scanners-config.yaml ├── .yamllint.yml ├── versions.yaml ├── CODEOWNERS ├── .run ├── Install CRDs.run.xml ├── Un-Deploy kyma.run.xml ├── Delete Test Clusters.run.xml ├── Deploy KLM from sources.run.xml ├── E2E Tests.run.xml ├── Ensure Test Clusters.run.xml ├── Deploy kyma.run.xml ├── Install Watcher Resources For Local KLM.run.xml ├── Deploy template-operator.run.xml ├── Deploy KLM from registry.run.xml ├── Create New Test Clusters.run.xml └── Launch KLM locally.run.xml ├── .markdownlint.yaml ├── api-version-compatibility-config.yaml ├── markdown_heading_capitalization.js ├── Dockerfile └── README.md /pkg/test_samples/empty-file.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/maintenancewindows/testdata/invalid-policy.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /maintenancewindows/resolver/testdata/ruleset-2.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /pkg/test_samples/oci/invalid-yaml.yaml: -------------------------------------------------------------------------------- 1 | onlyHaveKey 2 | --- 3 | -------------------------------------------------------------------------------- /unit-test-coverage-api.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | v1beta2: 3.8 3 | -------------------------------------------------------------------------------- /unit-test-coverage-maintenancewindows.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | resolver: 84 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "augustocdias.tasks-shell-input" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /config/watcher_local_test/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: kcp-system 5 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: controller-manager 5 | -------------------------------------------------------------------------------- /config/watcher_local_test_gcm/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: kcp-system 5 | -------------------------------------------------------------------------------- /api/shared/external_labels.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | const ( 4 | InstanceIDLabel = "kyma-project.io/instance-id" 5 | ) 6 | -------------------------------------------------------------------------------- /pkg/common/error.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "errors" 4 | 5 | var ErrTypeAssert = errors.New("type assertion failed") 6 | -------------------------------------------------------------------------------- /pkg/log/levels.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | const ( 4 | WarnLevel = iota 5 | InfoLevel 6 | DebugLevel 7 | TraceLevel 8 | ) 9 | -------------------------------------------------------------------------------- /pkg/test_samples/oci/rendered.tar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kyma-project/lifecycle-manager/HEAD/pkg/test_samples/oci/rendered.tar -------------------------------------------------------------------------------- /internal/errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import "errors" 4 | 5 | var ErrSkrClientNotFound = errors.New("SKR client not in cache") 6 | -------------------------------------------------------------------------------- /maintenancewindows/resolver/testdata/ruleset-3.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | - match: 3 | plan: "trial" 4 | - match: 5 | plan: "free" 6 | -------------------------------------------------------------------------------- /pkg/test_samples/oci/valid-yaml.yaml: -------------------------------------------------------------------------------- 1 | invalidKey: invalidValue 2 | kind: InvalidKind1 3 | --- 4 | invalidKey: invalidValue 5 | kind: InvalidKind2 6 | -------------------------------------------------------------------------------- /internal/result/result.go: -------------------------------------------------------------------------------- 1 | package result 2 | 3 | type UseCase string 4 | 5 | type Result struct { 6 | UseCase UseCase 7 | Err error 8 | } 9 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | testbin/ 5 | -------------------------------------------------------------------------------- /config/samples/.gitignore: -------------------------------------------------------------------------------- 1 | **/secret-gardener-k8s.yaml 2 | **/*.pem 3 | **/*component-descriptor*.yaml 4 | **/*generated* 5 | **/example 6 | secret/k3d-secret-gen.sh -------------------------------------------------------------------------------- /pkg/templatelookup/common/errors.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "errors" 4 | 5 | var ErrNoTemplatesInListResult = errors.New("no templates were found") 6 | -------------------------------------------------------------------------------- /api/shared/namespace.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | const ( 4 | DefaultControlPlaneNamespace = "kcp-system" 5 | DefaultRemoteNamespace = "kyma-system" 6 | ) 7 | -------------------------------------------------------------------------------- /internal/service/watcher/certificate/name/name.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | func SkrCertificate(kymaName string) string { 4 | return kymaName + "-webhook-tls" 5 | } 6 | -------------------------------------------------------------------------------- /internal/errors/kyma/deletion/errors.go: -------------------------------------------------------------------------------- 1 | package deletion 2 | 3 | import "errors" 4 | 5 | var ErrNoUseCaseApplicable = errors.New("no use case applicable for kyma deletion") 6 | -------------------------------------------------------------------------------- /scripts/tests/clusters_cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Remove the k3d cluster and the skr cluster 4 | k3d cluster rm kcp skr 5 | 6 | echo "[$(basename $0)] Cleanup completed" 7 | -------------------------------------------------------------------------------- /internal/errors/mandatorymodule/deletion/errors.go: -------------------------------------------------------------------------------- 1 | package deletion 2 | 3 | import "errors" 4 | 5 | var ErrMrmNotInDeletingState = errors.New("ModuleReleaseMeta not in deleting state") 6 | -------------------------------------------------------------------------------- /internal/errors/mandatorymodule/installation/errors.go: -------------------------------------------------------------------------------- 1 | package installation 2 | 3 | import "errors" 4 | 5 | var ErrSkippingReconciliationKyma = errors.New("skipping reconciliation for Kyma") 6 | -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | # Lifecycle Manager API 2 | 3 | To see Lifecycle Manager API documentation, go to the [Lifecycle Manager API](/docs/contributor/resources/README.md) document in the `/docs` folder. 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | To contribute to this project, follow the general [Contributing Rules](https://github.com/kyma-project/community/blob/main/docs/contributing/02-contributing.md). 4 | -------------------------------------------------------------------------------- /internal/watch/change_handler_client.go: -------------------------------------------------------------------------------- 1 | package watch 2 | 3 | import ( 4 | "sigs.k8s.io/controller-runtime/pkg/client" 5 | ) 6 | 7 | type ChangeHandlerClient interface { 8 | client.Reader 9 | } 10 | -------------------------------------------------------------------------------- /api/shared/external_annotations.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | const ( 4 | // This annotation carries the FQDN of the shoot cluster, for example: ".kyma.ondemand.com". 5 | SkrDomainAnnotation = "skr-domain" 6 | ) 7 | -------------------------------------------------------------------------------- /internal/common/fieldowners/const.go: -------------------------------------------------------------------------------- 1 | package fieldowners 2 | 3 | import "sigs.k8s.io/controller-runtime/pkg/client" 4 | 5 | const ( 6 | LifecycleManager = client.FieldOwner("operator.kyma-project.io/lifecycle-manager") 7 | ) 8 | -------------------------------------------------------------------------------- /.mlc.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "replacementPatterns": [ 3 | { 4 | "_comment": "a replacement rule for all the in-repository references", 5 | "pattern": "^/", 6 | "replacement": "{{BASEURL}}/" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /config/istio/patches/deployment_istio_inject.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | spec: 6 | template: 7 | metadata: 8 | labels: 9 | sidecar.istio.io/inject: "true" 10 | -------------------------------------------------------------------------------- /docs/user/README.md: -------------------------------------------------------------------------------- 1 | # Lifecycle Manager User Documentation 2 | 3 | * [Kyma Custom Resource](01-10-kyma-crd.md) 4 | * [Setting Your Module to the Unmanaged and Managed State](./02-unmanaging-modules.md) 5 | * [Skipping Maintenance Windows](./03-skipping-maintenance-windows.md) 6 | -------------------------------------------------------------------------------- /config/istio/patches/deployment_exclude_webhook_port.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | spec: 6 | template: 7 | metadata: 8 | annotations: 9 | traffic.sidecar.istio.io/excludeInboundPorts: "9443" 10 | -------------------------------------------------------------------------------- /internal/resources.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 4 | 5 | // ManifestResources holds a collection of objects, so that we can filter / sequence them. 6 | type ManifestResources struct { 7 | Items []*unstructured.Unstructured 8 | } 9 | -------------------------------------------------------------------------------- /tests/e2e/module_status_decoupling_with_deployment_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | ) 6 | 7 | var _ = Describe("Module Status Decoupling With Deployment", Ordered, func() { 8 | RunModuleStatusDecouplingTest(DeploymentKind) 9 | }) 10 | -------------------------------------------------------------------------------- /tests/e2e/module_status_decoupling_with_statefulset_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | ) 6 | 7 | var _ = Describe("Module Status Decoupling With StatefulSet", Ordered, func() { 8 | RunModuleStatusDecouplingTest(StatefulSetKind) 9 | }) 10 | -------------------------------------------------------------------------------- /config/webhook/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: webhook-service 5 | spec: 6 | ports: 7 | - port: 443 8 | protocol: TCP 9 | targetPort: 9443 10 | selector: 11 | app.kubernetes.io/component: lifecycle-manager.kyma-project.io 12 | -------------------------------------------------------------------------------- /internal/common/fieldindex/indexed_fields.go: -------------------------------------------------------------------------------- 1 | package fieldindex 2 | 3 | const ( 4 | MrmMandatoryModuleName = ".spec.mandatory" 5 | MrmMandatoryModulePositiveValue = "true" 6 | MrmMandatoryModuleNegativeValue = "false" 7 | 8 | ModuleTemplateVersionName = ".spec.version" 9 | ) 10 | -------------------------------------------------------------------------------- /config/watcher_local_test/patches/fips_only_mode.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: manager 10 | env: 11 | - name: GODEBUG 12 | value: "fips140=only,tlsmlkem=0" 13 | -------------------------------------------------------------------------------- /config/istio/commonlabels_override.yaml: -------------------------------------------------------------------------------- 1 | # This is an extension to builtin commonLabels, 2 | # reference https://github.com/kubernetes-sigs/kustomize/blob/master/api/konfig/builtinpluginconsts/commonlabels.go 3 | commonLabels: 4 | - path: spec/selector/matchLabels 5 | create: true 6 | version: v1beta1 7 | kind: AuthorizationPolicy 8 | -------------------------------------------------------------------------------- /config/watcher_local_test_gcm/patches/fips_only_mode.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: manager 10 | env: 11 | - name: GODEBUG 12 | value: "fips140=only,tlsmlkem=0" 13 | -------------------------------------------------------------------------------- /maintenancewindows/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kyma-project/lifecycle-manager/maintenancewindows 2 | 3 | go 1.25.5 4 | 5 | require github.com/stretchr/testify v1.11.1 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /scripts/tests/undeploy_kyma.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exporting the path to the kubeconfig file 4 | export KUBECONFIG=$HOME/.k3d/kcp-local.yaml 5 | 6 | # Undeploying Kyma 7 | kubectl -n kcp-system delete kyma kyma-sample 8 | kubectl -n kcp-system delete secret kyma-sample 9 | 10 | echo "[$(basename $0)] Kyma undeployed successfully" 11 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | resources: 4 | - service_account.yaml 5 | - manager_role.yaml 6 | - manager_role_binding.yaml 7 | - leader_election_role.yaml 8 | - leader_election_role_binding.yaml 9 | - crd_cluster_role.yaml 10 | - crd_cluster_role_binding.yaml 11 | -------------------------------------------------------------------------------- /.mockery.yaml: -------------------------------------------------------------------------------- 1 | with-expecter: true 2 | dir: "{{.InterfaceDir}}/testutils" 3 | mockname: "{{.InterfaceName}}Mock" 4 | outpkg: "testutils" 5 | filename: "mock_{{.InterfaceName}}.go" 6 | inpackage: true 7 | issue-845-fix: true 8 | packages: 9 | github.com/kyma-project/lifecycle-manager/internal/gatewaysecret: 10 | interfaces: 11 | Client: 12 | -------------------------------------------------------------------------------- /internal/service/mandatorymodule/deletion/usecases/event.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | machineryruntime "k8s.io/apimachinery/pkg/runtime" 5 | 6 | "github.com/kyma-project/lifecycle-manager/internal/event" 7 | ) 8 | 9 | type EventHandler interface { 10 | Warning(object machineryruntime.Object, reason event.Reason, err error) 11 | } 12 | -------------------------------------------------------------------------------- /config/rbac/crd_cluster_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: controller-manager-crds 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: controller-manager-crds 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | -------------------------------------------------------------------------------- /api/shared/cr_finalizer.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | const ( 4 | KymaFinalizer = OperatorGroup + Separator + string(KymaKind) 5 | PurgeFinalizer = OperatorGroup + Separator + "purge-finalizer" 6 | WatcherFinalizer = OperatorGroup + Separator + "watcher" 7 | MandatoryModuleFinalizer = OperatorGroup + Separator + "mandatory-module" 8 | ) 9 | -------------------------------------------------------------------------------- /config/certmanager/patches/cainjection_in_kymas.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: kymas.operator.kyma-project.io 8 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | generatorOptions: 4 | disableNameSuffixHash: true 5 | resources: 6 | - manager.yaml 7 | - metrics_service.yaml 8 | images: 9 | - name: controller 10 | newName: europe-docker.pkg.dev/kyma-project/prod/lifecycle-manager 11 | newTag: latest 12 | -------------------------------------------------------------------------------- /config/rbac/manager_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: controller-manager 5 | namespace: kcp-system 6 | roleRef: 7 | apiGroup: rbac.authorization.k8s.io 8 | kind: Role 9 | name: controller-manager 10 | subjects: 11 | - kind: ServiceAccount 12 | name: controller-manager 13 | -------------------------------------------------------------------------------- /maintenancewindows/README.md: -------------------------------------------------------------------------------- 1 | # Maitenance Windows Library for Kyma 2 | 3 | ## Overview 4 | 5 | This module contains the code for calculating the maintenance windows during which a Kyma cluster can be updated. [Lifecycle Manager](https://github.com/kyma-project/lifecycle-manager) and other components of the Kyma Control Plane use this maintenance windows library. 6 | 7 | -------------------------------------------------------------------------------- /config/certmanager/patches/cainjection_in_manifests.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: manifests.operator.kyma-project.io 8 | -------------------------------------------------------------------------------- /config/certmanager/patches/cainjection_in_watchers.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: watchers.operator.kyma-project.io 8 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: controller-manager-leader-election 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: controller-manager-leader-election 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | -------------------------------------------------------------------------------- /internal/repository/watcher/certificate/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "time" 4 | 5 | // CertificateValues contains the configuration for the certificates the repository implementations will create. 6 | type CertificateValues struct { 7 | Namespace string 8 | Duration time.Duration 9 | RenewBefore time.Duration 10 | KeySize int 11 | } 12 | -------------------------------------------------------------------------------- /config/certmanager/patches/cainjection_in_moduletemplates.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: moduletemplates.operator.kyma-project.io 8 | -------------------------------------------------------------------------------- /tests/e2e/kyma_deprovision_background_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | ) 7 | 8 | var _ = Describe("KCP Kyma CR Deprovision With Background Propagation After SKR Cluster Removal", Ordered, 9 | func() { 10 | RunDeletionTest(apimetav1.DeletePropagationBackground) 11 | }) 12 | -------------------------------------------------------------------------------- /config/webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | resources: 4 | - service.yaml 5 | configurations: 6 | - kustomizeconfig.yaml 7 | patches: 8 | - target: 9 | kind: Deployment 10 | patch: |- 11 | - op: add 12 | path: /spec/template/spec/containers/0/args/- 13 | value: --enable-webhooks=true 14 | -------------------------------------------------------------------------------- /hack/k3d-secret-gen.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | echo " 3 | apiVersion: v1 4 | kind: Secret 5 | metadata: 6 | name: $1 7 | namespace: $2 8 | labels: 9 | operator.kyma-project.io/managed-by: lifecycle-manager 10 | operator.kyma-project.io/kyma-name: $1 11 | type: Opaque 12 | data: 13 | config: $(kubectl config view --raw --minify | sed 's/---//g' | base64)" > $1-secret.yaml 14 | -------------------------------------------------------------------------------- /scripts/tests/install_crds.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Change to root directory of the project 4 | cd "$(git rev-parse --show-toplevel)" 5 | 6 | # Exporting necessary environment variables 7 | export KUBECONFIG=${HOME}/.k3d/kcp-local.yaml 8 | 9 | # Install CRDs 10 | echo "[$(basename $0)] Installing CRDs" 11 | make install 12 | echo "[$(basename $0)] CRDs installed successfully" 13 | -------------------------------------------------------------------------------- /tests/e2e/kyma_deprovision_foreground_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | ) 8 | 9 | var _ = Describe("KCP Kyma CR Deprovision With Foreground Propagation After SKR Cluster Removal", Ordered, 10 | func() { 11 | RunDeletionTest(apimetav1.DeletePropagationForeground) 12 | }) 13 | -------------------------------------------------------------------------------- /internal/declarative/v2/spec.go: -------------------------------------------------------------------------------- 1 | package v2 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyma-project/lifecycle-manager/api/v1beta2" 7 | ) 8 | 9 | type SpecResolver interface { 10 | GetSpec(ctx context.Context, manifest *v1beta2.Manifest) (*Spec, error) 11 | } 12 | 13 | type Spec struct { 14 | ManifestName string 15 | Path string 16 | OCIRef string 17 | } 18 | -------------------------------------------------------------------------------- /api/shared/last_operation.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | // LastOperation defines the last operation from the control-loop. 8 | // +k8s:deepcopy-gen=true 9 | type LastOperation struct { 10 | Operation string `json:"operation"` 11 | LastUpdateTime apimetav1.Time `json:"lastUpdateTime,omitempty"` 12 | } 13 | -------------------------------------------------------------------------------- /api/v1beta1/last_operation.go: -------------------------------------------------------------------------------- 1 | package v1beta1 2 | 3 | import ( 4 | apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | // LastOperation defines the last operation from the control-loop. 8 | // +k8s:deepcopy-gen=true 9 | type LastOperation struct { 10 | Operation string `json:"operation"` 11 | LastUpdateTime apimetav1.Time `json:"lastUpdateTime,omitempty"` 12 | } 13 | -------------------------------------------------------------------------------- /config/certmanager/certmanager_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: controller-manager-certmanager 5 | namespace: istio-system 6 | roleRef: 7 | apiGroup: rbac.authorization.k8s.io 8 | kind: Role 9 | name: controller-manager-certmanager 10 | subjects: 11 | - kind: ServiceAccount 12 | name: controller-manager 13 | -------------------------------------------------------------------------------- /config/istio/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | generatorOptions: 4 | disableNameSuffixHash: true 5 | configurations: 6 | - commonlabels_override.yaml 7 | resources: 8 | - endpoints_authorization_policy.yaml 9 | patches: 10 | - path: patches/deployment_istio_inject.yaml 11 | - path: patches/deployment_exclude_webhook_port.yaml 12 | -------------------------------------------------------------------------------- /internal/declarative/v2/object.go: -------------------------------------------------------------------------------- 1 | package v2 2 | 3 | import ( 4 | "sigs.k8s.io/controller-runtime/pkg/client" 5 | 6 | "github.com/kyma-project/lifecycle-manager/api/shared" 7 | ) 8 | 9 | //go:generate mockgen -source object.go -destination mock/object.go Object 10 | type Object interface { 11 | client.Object 12 | GetStatus() shared.Status 13 | SetStatus(status shared.Status) 14 | } 15 | -------------------------------------------------------------------------------- /internal/descriptor/cache/descriptorkey.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/kyma-project/lifecycle-manager/internal/descriptor/types/ocmidentity" 7 | ) 8 | 9 | type DescriptorKey string 10 | 11 | func GenerateDescriptorKey(ocmId ocmidentity.ComponentId) DescriptorKey { 12 | return DescriptorKey(fmt.Sprintf("%s:%s", ocmId.Name(), ocmId.Version())) 13 | } 14 | -------------------------------------------------------------------------------- /api/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: vet 2 | vet: ## Run go vet against code. 3 | go vet ./... 4 | 5 | .PHONY: fmt 6 | fmt: ## Run go fmt against code. 7 | go fmt ./... 8 | 9 | .PHONY: build-verbose 10 | build-verbose: 11 | GOFIPS140=v1.0.0 go build -v ./... 12 | 13 | ##@ Testing 14 | 15 | .PHONY: test 16 | test: fmt vet ## Run tests. 17 | GOFIPS140=v1.0.0 go test ./... -v --race -coverprofile cover.out 18 | -------------------------------------------------------------------------------- /api/shared/istio.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | const ( 4 | CACertificateName = "klm-watcher-serving" 5 | IstioNamespace = "istio-system" 6 | GatewaySecretName = "klm-istio-gateway" //nolint:gosec // It is just a name 7 | LastModifiedAtAnnotation = "lastModifiedAt" 8 | GCMSecretAnnotation = "cert.gardener.cloud/requestedAt" //nolint:gosec // It is just a name 9 | ) 10 | -------------------------------------------------------------------------------- /scripts/tests/apply_cm_certificate_management.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CERT_MANAGER_VERSION=$1 4 | 5 | kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v${CERT_MANAGER_VERSION}/cert-manager.yaml 6 | for deploy in cert-manager cert-manager-webhook cert-manager-cainjector; do 7 | kubectl rollout status deploy -n cert-manager "$deploy" --timeout=2m || exit 1 8 | done 9 | -------------------------------------------------------------------------------- /api/shared/channel.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import "strings" 4 | 5 | type Channel string 6 | 7 | const ( 8 | // NoneChannel when this value is defined for the ModuleTemplate, 9 | // it means that the ModuleTemplate is not assigned to any channel. 10 | NoneChannel Channel = "none" 11 | ) 12 | 13 | func (c Channel) Equals(value string) bool { 14 | return string(c) == strings.ToLower(value) 15 | } 16 | -------------------------------------------------------------------------------- /config/control-plane/patches/deployment_resources.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: manager 10 | resources: 11 | limits: 12 | memory: 4000Mi 13 | requests: 14 | cpu: 1000m 15 | memory: 1000Mi 16 | -------------------------------------------------------------------------------- /config/samples/component-integration-installed/operator_v1beta2_kyma.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operator.kyma-project.io/v1beta2 2 | kind: Kyma 3 | metadata: 4 | labels: 5 | "operator.kyma-project.io/managed-by": "lifecycle-manager" 6 | "operator.kyma-project.io/beta": "true" 7 | name: default-kyma 8 | namespace: kcp-system 9 | spec: 10 | channel: regular 11 | modules: 12 | - name: template-operator 13 | -------------------------------------------------------------------------------- /maintenancewindows/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: vet 2 | vet: ## Run go vet against code. 3 | go vet ./... 4 | 5 | .PHONY: fmt 6 | fmt: ## Run go fmt against code. 7 | go fmt ./... 8 | 9 | .PHONY: build-verbose 10 | build-verbose: 11 | GOFIPS140=v1.0.0 go build -v ./... 12 | 13 | ##@ Testing 14 | 15 | .PHONY: test 16 | test: fmt vet ## Run tests. 17 | GOFIPS140=v1.0.0 go test ./... -v --race -coverprofile cover.out 18 | -------------------------------------------------------------------------------- /scripts/tests/ocm-config-local-registry.yaml: -------------------------------------------------------------------------------- 1 | type: generic.config.ocm.software/v1 2 | configurations: 3 | - type: ocm.config.ocm.software 4 | aliases: 5 | localhost:5111: 6 | type: OCIRegistry 7 | baseUrl: http://localhost:5111 8 | - type: oci.config.ocm.software 9 | aliases: 10 | localhost:5111: 11 | type: OCIRegistry 12 | baseUrl: http://localhost:5111 13 | -------------------------------------------------------------------------------- /.github/actions/patch-skr-hostname-in-kcp/action.yml: -------------------------------------------------------------------------------- 1 | name: Patch SKR hostname in the KCP cluster 2 | description: Redirects hostname "skr.cluster.local" to the SKR cluster in the KCP cluster 3 | runs: 4 | using: composite 5 | steps: 6 | - name: Patch SKR hostname in the KCP cluster 7 | working-directory: ./lifecycle-manager 8 | shell: bash 9 | run: ./scripts/tests/add_skr_host_to_coredns.sh 10 | -------------------------------------------------------------------------------- /config/samples/component-integration-installed/v1_secret_remote_cluster_access.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: kyma-sample 5 | namespace: default 6 | labels: 7 | "operator.kyma-project.io/kyma-name": "kyma-sample" 8 | "operator.kyma-project.io/managed-by": "lifecycle-manager" 9 | data: 10 | config: # Replace with a config of your k8s cluster 11 | -------------------------------------------------------------------------------- /api/scheme.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | 6 | machineryruntime "k8s.io/apimachinery/pkg/runtime" 7 | 8 | "github.com/kyma-project/lifecycle-manager/api/v1beta2" 9 | ) 10 | 11 | func AddToScheme(scheme *machineryruntime.Scheme) error { 12 | if err := v1beta2.AddToScheme(scheme); err != nil { 13 | return fmt.Errorf("failed to add scheme on v1beta2 api: %w", err) 14 | } 15 | 16 | return nil 17 | } 18 | -------------------------------------------------------------------------------- /config/gardener-certmanager/gardener_certmanager_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: controller-manager-gardener-certmanager 5 | namespace: istio-system 6 | roleRef: 7 | apiGroup: rbac.authorization.k8s.io 8 | kind: Role 9 | name: controller-manager-gardener-certmanager 10 | subjects: 11 | - kind: ServiceAccount 12 | name: controller-manager 13 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Each contributor and maintainer of this project agrees to follow the [community Code of Conduct](https://github.com/kyma-project/community/blob/main/docs/contributing/01-code-of-conduct.md) that relies on the CNCF Code of Conduct. Read it to learn about the agreed standards of behavior, shared values that govern our community, and details on how to report any suspected Code of Conduct violations. -------------------------------------------------------------------------------- /tests/integration/paths.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "path" 5 | "runtime" 6 | ) 7 | 8 | const ( 9 | prjRoot = "../.." 10 | ) 11 | 12 | // GetProjectRoot returns the absolute path to the project's root. 13 | func GetProjectRoot() string { 14 | _, filename, _, _ := runtime.Caller(0) //nolint:dogsled // Reading only the filename from the caller. 15 | 16 | return path.Join(path.Dir(filename), prjRoot) 17 | } 18 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | resources: 4 | - bases/operator.kyma-project.io_kymas.yaml 5 | - bases/operator.kyma-project.io_manifests.yaml 6 | - bases/operator.kyma-project.io_moduletemplates.yaml 7 | - bases/operator.kyma-project.io_watchers.yaml 8 | - bases/operator.kyma-project.io_modulereleasemetas.yaml 9 | configurations: 10 | - kustomizeconfig.yaml 11 | -------------------------------------------------------------------------------- /config/watcher_local_test/patches/deployment_resources.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: manager 10 | resources: 11 | limits: 12 | cpu: 400m 13 | memory: 400Mi 14 | requests: 15 | cpu: 100m 16 | memory: 100Mi 17 | -------------------------------------------------------------------------------- /config/watcher_local_test_gcm/patches/deployment_resources.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: manager 10 | resources: 11 | limits: 12 | cpu: 400m 13 | memory: 400Mi 14 | requests: 15 | cpu: 100m 16 | memory: 100Mi 17 | -------------------------------------------------------------------------------- /scripts/tests/remove_skr_host_from_coredns.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SKR_HOSTNAME="skr.cluster.local" 4 | 5 | UPDATED_ENTRIES=$(kubectl get configmap coredns -n kube-system -o yaml | yq e '.data.NodeHosts' - | grep -v "$SKR_HOSTNAME") 6 | 7 | kubectl get configmap coredns -n kube-system -o yaml | \ 8 | yq e ".data.NodeHosts = \"$UPDATED_ENTRIES\"" - | \ 9 | kubectl apply -f - 10 | 11 | kubectl rollout restart -n kube-system deployment coredns -------------------------------------------------------------------------------- /config/manager/metrics_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app.kubernetes.io/component: lifecycle-manager.kyma-project.io 6 | name: controller-manager-metrics 7 | spec: 8 | ports: 9 | - name: metrics 10 | port: 8080 11 | protocol: TCP 12 | appProtocol: http 13 | targetPort: metrics 14 | selector: 15 | app.kubernetes.io/component: lifecycle-manager.kyma-project.io 16 | -------------------------------------------------------------------------------- /.github/actions/switch-kubectl-context/action.yml: -------------------------------------------------------------------------------- 1 | name: Switch kubectl context 2 | description: Switches kubectl to use the context with the provided name. 3 | inputs: 4 | context_name: 5 | description: The name of the context to use. 6 | required: true 7 | runs: 8 | using: composite 9 | steps: 10 | - name: Configure kubectl 11 | shell: bash 12 | run: | 13 | kubectl config use-context ${{ inputs.context_name }} 14 | -------------------------------------------------------------------------------- /config/gardener-certmanager/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | #This configuration is for teaching kustomize how to update name ref and var substitution 2 | nameReference: 3 | - kind: Issuer 4 | group: cert.gardener.cloud 5 | fieldSpecs: 6 | - kind: Certificate 7 | group: cert.gardener.cloud 8 | path: spec/issuerRef/name 9 | varReference: 10 | - kind: Certificate 11 | group: cert.gardener.cloud 12 | path: spec/dnsNames 13 | -------------------------------------------------------------------------------- /internal/repository/watcher/certificate/errors/error.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrNoRenewalTime = errors.New("no renewal time set for certificate") 7 | ErrNoNotBefore = errors.New("notBefore not found") 8 | ErrNoNotAfter = errors.New("notAfter not found") 9 | ErrCertRepoConfigNamespace = errors.New("repository needs to be initialized with a namespace for certificates") 10 | ) 11 | -------------------------------------------------------------------------------- /pkg/test_samples/auth_secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | .dockerconfigjson: eyJhdXRocyI6eyJ0ZXN0LnJlZ2lzdHJ5LmlvIjp7InVzZXJuYW1lIjoidGVzdF91c2VyIiwicGFzc3dvcmQiOiJ0ZXN0X3Bhc3MiLCJhdXRoIjoiZEdWemRGOTFjMlZ5T25SbGMzUmZjR0Z6Y3c9PSJ9fX0= 4 | kind: Secret 5 | metadata: 6 | name: private-oci-registry-cred 7 | namespace: kcp-system 8 | labels: 9 | operator.kyma-project.io/managed-by: lifecycle-manager 10 | type: kubernetes.io/dockerconfigjson 11 | -------------------------------------------------------------------------------- /internal/service/kyma/deletion/usecases/types.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | 6 | "k8s.io/apimachinery/pkg/types" 7 | ) 8 | 9 | type SkrAccessSecretRepo interface { 10 | ExistsForKyma(ctx context.Context, kymaName string) (bool, error) 11 | } 12 | 13 | type ExistsDeleteRepo interface { 14 | Exists(ctx context.Context, kymaName types.NamespacedName) (bool, error) 15 | Delete(ctx context.Context, kymaName types.NamespacedName) error 16 | } 17 | -------------------------------------------------------------------------------- /internal/controller/README.md: -------------------------------------------------------------------------------- 1 | # Controllers used within Lifecycle Manager 2 | 3 | This package contains all controllers that can be registered within the Lifecycle Manager. 4 | For more details, read [Lifecycle Manager Controllers](/docs/contributor/02-controllers.md) in the `/docs` folder. 5 | For more information on how the API behaves after the controller finishes up the synchronization, please look at the [Lifecycle Manager API](/docs/contributor/resources/README.md) document. 6 | -------------------------------------------------------------------------------- /config/samples/tests/istio-test-resources.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1beta1 2 | kind: Gateway 3 | metadata: 4 | name: klm-watcher 5 | namespace: kcp-system 6 | labels: 7 | operator.kyma-project.io/watcher-gateway: default 8 | spec: 9 | selector: 10 | istio: ingressgateway 11 | servers: 12 | - hosts: 13 | - "listener.test.kyma.cloud.sap" 14 | port: 15 | name: http 16 | number: 80 17 | protocol: HTTP 18 | --- 19 | -------------------------------------------------------------------------------- /docs/user/03-skipping-maintenance-windows.md: -------------------------------------------------------------------------------- 1 | # Skipping Maintenance Windows 2 | 3 | ## Context 4 | 5 | In SAP BTP, Kyma runtime, modules' major upgrades happen rarely, in a bigger maintenance window. In some cases, these upgrades require downtime. You can decide not to wait for a maintenance window and upgrade a module version requiering downtime as soon as it is available. 6 | 7 | ## Procedure 8 | 9 | Set the **spec.skipMaintenanceWindow** field to `true` in the Kyma CR. 10 | -------------------------------------------------------------------------------- /internal/service/mandatorymodule/deletion/usecases/event_test.go: -------------------------------------------------------------------------------- 1 | package usecases_test 2 | 3 | import ( 4 | machineryruntime "k8s.io/apimachinery/pkg/runtime" 5 | 6 | "github.com/kyma-project/lifecycle-manager/internal/event" 7 | ) 8 | 9 | type mockEventHandler struct { 10 | Called bool 11 | Reason event.Reason 12 | } 13 | 14 | func (m *mockEventHandler) Warning(_ machineryruntime.Object, reason event.Reason, _ error) { 15 | m.Called = true 16 | m.Reason = reason 17 | } 18 | -------------------------------------------------------------------------------- /config/rbac/crd_cluster_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: controller-manager-crds 5 | rules: 6 | - apiGroups: 7 | - apiextensions.k8s.io 8 | resources: 9 | - customresourcedefinitions 10 | verbs: 11 | - get 12 | - list 13 | - watch 14 | - apiGroups: 15 | - apiextensions.k8s.io 16 | resources: 17 | - customresourcedefinitions/status 18 | verbs: 19 | - update 20 | -------------------------------------------------------------------------------- /pkg/test_samples/oci/manifest-contains-duplicate-resources.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/component: btp-manager.kyma-project.io 6 | name: btp-manager-controller-manager 7 | namespace: kyma-system 8 | --- 9 | apiVersion: v1 10 | kind: ServiceAccount 11 | metadata: 12 | labels: 13 | app.kubernetes.io/component: btp-manager.kyma-project.io 14 | name: btp-manager-controller-manager 15 | namespace: kyma-system 16 | -------------------------------------------------------------------------------- /.github/scripts/release/validate_release_tag.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o nounset 4 | set -o errexit 5 | set -E 6 | set -o pipefail 7 | 8 | CURRENT_RELEASE_TAG=$1 9 | 10 | semver_pattern="^([0-9]|[1-9][0-9]*)[.]([0-9]|[1-9][0-9]*)[.]([0-9]|[1-9][0-9]*)(-[a-z][a-z0-9]*)?$" 11 | 12 | if ! [[ $CURRENT_RELEASE_TAG =~ $semver_pattern ]]; then 13 | echo "Given tag \"$CURRENT_RELEASE_TAG\" does not match the expected semantic version pattern: \"$semver_pattern\"." 14 | exit 1 15 | fi -------------------------------------------------------------------------------- /config/istio/endpoints_authorization_policy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: security.istio.io/v1beta1 2 | kind: AuthorizationPolicy 3 | metadata: 4 | name: controller-manager 5 | spec: 6 | action: ALLOW 7 | rules: 8 | - to: 9 | - operation: 10 | paths: 11 | - /metrics 12 | - /v2* 13 | - /convert* 14 | - /mutate* 15 | selector: 16 | matchLabels: 17 | app.kubernetes.io/component: lifecycle-manager.kyma-project.io 18 | -------------------------------------------------------------------------------- /config/grafana/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | 4 | configMapGenerator: 5 | - name: dashboard-overview 6 | files: 7 | - overview.json 8 | - name: dashboard-status 9 | files: 10 | - status.json 11 | - name: dashboard-watcher 12 | files: 13 | - watcher.json 14 | - name: dashboard-mandatory-modules 15 | files: 16 | - mandatory-modules.json 17 | 18 | generatorOptions: 19 | disableNameSuffixHash: true 20 | -------------------------------------------------------------------------------- /internal/common/errors.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrUnsupportedCertificateManagementSystem = errors.New("unsupported certificate management system") 7 | ErrNoOCIRegistryHostAndCredSecret = errors.New( 8 | "the flags --oci-registry-host and --oci-registry-cred-secret cannot be both empty", 9 | ) 10 | ErrBothOCIRegistryHostAndCredSecretProvided = errors.New( 11 | "only one of --oci-registry-host or --oci-registry-cred-secret should be provided", 12 | ) 13 | ) 14 | -------------------------------------------------------------------------------- /config/maintenance_windows/patches/volume_mount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: klm-controller-manager 5 | namespace: kcp-system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | volumeMounts: 12 | - name: maintenance-policy 13 | mountPath: /etc/maintenance-policy 14 | readOnly: true 15 | volumes: 16 | - name: maintenance-policy 17 | configMap: 18 | name: klm-maintenance-config 19 | -------------------------------------------------------------------------------- /.github/actions/setup-https-server/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup HTTPS server 2 | description: Setup an HTTPS server to serve the template operator manifest file. 3 | inputs: 4 | directory_name: 5 | description: The name of the directory which contains the manifest file to serve. 6 | required: true 7 | runs: 8 | using: composite 9 | steps: 10 | - name: Serve HTTPs server 11 | working-directory: ./lifecycle-manager 12 | shell: bash 13 | run: | 14 | ./scripts/tests/setup_https_server.sh ${{ inputs.directory_name }} 15 | -------------------------------------------------------------------------------- /api/shared/cr_kind.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | KymaKind Kind = "Kyma" 10 | ModuleTemplateKind Kind = "ModuleTemplate" 11 | WatcherKind Kind = "Watcher" 12 | ManifestKind Kind = "Manifest" 13 | ModuleReleaseMetaKind Kind = "ModuleReleaseMeta" 14 | ) 15 | 16 | type Kind string 17 | 18 | func (k Kind) Plural() string { 19 | return strings.ToLower(string(k)) + "s" 20 | } 21 | 22 | func (k Kind) List() string { 23 | return fmt.Sprintf("%sList", k) 24 | } 25 | -------------------------------------------------------------------------------- /api/shared/cr_kind_test.go: -------------------------------------------------------------------------------- 1 | package shared_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/kyma-project/lifecycle-manager/api/shared" 9 | ) 10 | 11 | func TestKind_Plural(t *testing.T) { 12 | kymaKind := shared.KymaKind 13 | 14 | kymaPlural := kymaKind.Plural() 15 | 16 | assert.Equal(t, "kymas", kymaPlural) 17 | } 18 | 19 | func TestKind_List(t *testing.T) { 20 | kymaKind := shared.KymaKind 21 | 22 | kymaList := kymaKind.List() 23 | 24 | assert.Equal(t, "KymaList", kymaList) 25 | } 26 | -------------------------------------------------------------------------------- /cmd/composition/provider/componentdescriptorcache/provider.go: -------------------------------------------------------------------------------- 1 | package componentdescriptorcache 2 | 3 | import ( 4 | "github.com/kyma-project/lifecycle-manager/internal/descriptor/provider" 5 | ) 6 | 7 | // ComposeComponentDescriptorService manges creation of a new instance of the Cached ComponentDescriptor Provider. 8 | func ComposeCachedDescriptorProvider( 9 | service provider.DescriptorService, 10 | cache provider.DescriptorCache, 11 | ) *provider.CachedDescriptorProvider { 12 | return provider.NewCachedDescriptorProvider(service, cache) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/testutils/ocm.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "github.com/kyma-project/lifecycle-manager/internal/descriptor/types/ocmidentity" 5 | ) 6 | 7 | const DefaultFQDN = "kyma-project.io/module/template-operator" 8 | 9 | // MustNewComponentId is a convenience ComponentId constructor that panics if name or version are not provided. 10 | func MustNewComponentId(name, version string) *ocmidentity.ComponentId { 11 | ocmId, err := ocmidentity.NewComponentId(name, version) 12 | if err != nil { 13 | panic(err) 14 | } 15 | return ocmId 16 | } 17 | -------------------------------------------------------------------------------- /internal/manifest/keychainprovider/default.go: -------------------------------------------------------------------------------- 1 | package keychainprovider 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/google/go-containerregistry/pkg/authn" 7 | "github.com/google/go-containerregistry/pkg/v1/google" 8 | ) 9 | 10 | type DefaultKeychainProvider struct{} 11 | 12 | func NewDefaultKeyChainProvider() *DefaultKeychainProvider { 13 | return &DefaultKeychainProvider{} 14 | } 15 | 16 | func (a *DefaultKeychainProvider) Get(_ context.Context) (authn.Keychain, error) { 17 | return authn.NewMultiKeychain(google.Keychain, authn.DefaultKeychain), nil 18 | } 19 | -------------------------------------------------------------------------------- /scripts/tests/e2e.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$#" -ne 1 ]; then 4 | echo "Usage: $0 [name of test target]" 5 | exit 1 6 | fi 7 | 8 | # Changing to root directory of the repository 9 | cd "$(git rev-parse --show-toplevel)" 10 | 11 | # Exporting necessary environment variables 12 | export KCP_KUBECONFIG=${HOME}/.k3d/kcp-local.yaml 13 | export SKR_KUBECONFIG=${HOME}/.k3d/skr-local.yaml 14 | 15 | # Execute E2E Tests 16 | echo "[$(basename $0)] Running E2E Tests" 17 | # Error handling for invalid test target shall be handled by the Makefile 18 | make -C tests/e2e $1 19 | -------------------------------------------------------------------------------- /internal/service/kyma/deletion/usecases/types_test.go: -------------------------------------------------------------------------------- 1 | package usecases_test 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyma-project/lifecycle-manager/internal/service/kyma/deletion/usecases" 7 | ) 8 | 9 | type skrAccessSecretRepoStub struct { 10 | usecases.SkrAccessSecretRepo 11 | 12 | called bool 13 | kymaName string 14 | exists bool 15 | err error 16 | } 17 | 18 | func (r *skrAccessSecretRepoStub) ExistsForKyma(_ context.Context, kymaName string) (bool, error) { 19 | r.called = true 20 | r.kymaName = kymaName 21 | return r.exists, r.err 22 | } 23 | -------------------------------------------------------------------------------- /.github/actions/install-yq/action.yml: -------------------------------------------------------------------------------- 1 | name: Install yq 2 | description: Downloads yq and installs it locally. 3 | inputs: 4 | yq_version: 5 | description: The version of yq to install. For example, 4.45.1. 6 | required: true 7 | runs: 8 | using: composite 9 | steps: 10 | - name: Install yq 11 | shell: bash 12 | run: | 13 | mkdir -p ./yq/bin 14 | wget https://github.com/mikefarah/yq/releases/${{ inputs.yq_version }}/download/yq_linux_amd64 -o ./yq/bin/yq &&\ 15 | chmod +x ./yq/bin/yq 16 | echo "$(pwd)/yq/bin" >> $GITHUB_PATH 17 | -------------------------------------------------------------------------------- /.github/scripts/release/publish_release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o nounset 4 | set -o errexit 5 | set -E 6 | set -o pipefail 7 | 8 | RELEASE_VERSION=$1 9 | 10 | GITHUB_URL=https://api.github.com/repos/${CODE_REPOSITORY} 11 | GITHUB_AUTH_HEADER="Authorization: Bearer ${GITHUB_TOKEN}" 12 | 13 | CURL_RESPONSE=$(curl -L \ 14 | -X POST \ 15 | -H "Accept: application/vnd.github+json" \ 16 | -H "${GITHUB_AUTH_HEADER}" \ 17 | -H "X-GitHub-Api-Version: 2022-11-28" \ 18 | "${GITHUB_URL}"/releases/"${RELEASE_VERSION}" \ 19 | -d '{"draft":false}') 20 | echo "$CURL_RESPONSE" 21 | -------------------------------------------------------------------------------- /pkg/status/conditions.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | 6 | "github.com/kyma-project/lifecycle-manager/api/v1beta2" 7 | ) 8 | 9 | // InitConditions initializes the required conditions in the Kyma CR. 10 | func InitConditions(kyma *v1beta2.Kyma, watcherEnabled, skrImagePullSecretEnabled bool) { 11 | kyma.Status.Conditions = []apimetav1.Condition{} 12 | for _, cond := range v1beta2.GetRequiredConditionTypes(watcherEnabled, skrImagePullSecretEnabled) { 13 | kyma.UpdateCondition(cond, apimetav1.ConditionUnknown) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/scripts/release/wait_for_image.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o nounset 4 | set -o errexit 5 | set -E 6 | set -o pipefail 7 | 8 | DOCKER_IMAGE=$1 9 | ITERATIONS=${2:-30} 10 | SLEEP_TIME="${3:-30}" 11 | 12 | for (( c=1; c<=ITERATIONS; c++ )) 13 | do 14 | if docker manifest inspect "$DOCKER_IMAGE" > /dev/null 2>&1; then 15 | exit 0 16 | fi 17 | echo "Attempt $c: Docker image: $DOCKER_IMAGE doesn't exist" 18 | if [[ $c -lt $ITERATIONS ]]; then 19 | sleep "$SLEEP_TIME" 20 | fi 21 | done 22 | 23 | echo "Fail: Docker image: $DOCKER_IMAGE doesn't exist" 24 | exit 1 25 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # Configure name and namespace reference substitution in CRDs 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | namespace: 11 | - kind: CustomResourceDefinition 12 | version: v1 13 | group: apiextensions.k8s.io 14 | path: spec/conversion/webhook/clientConfig/service/namespace 15 | create: false 16 | varReference: 17 | - path: metadata/annotations 18 | -------------------------------------------------------------------------------- /internal/repository/watcher/certificate/common_test.go: -------------------------------------------------------------------------------- 1 | package certificate_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/kyma-project/lifecycle-manager/api/shared" 9 | "github.com/kyma-project/lifecycle-manager/internal/repository/watcher/certificate" 10 | ) 11 | 12 | func Test_GetCertificateLabels(t *testing.T) { 13 | labels := certificate.GetCertificateLabels() 14 | 15 | assert.Len(t, labels, 2) 16 | assert.Equal(t, shared.CertManager, labels[shared.PurposeLabel]) 17 | assert.Equal(t, shared.OperatorName, labels[shared.ManagedBy]) 18 | } 19 | -------------------------------------------------------------------------------- /sec-scanners-config.yaml: -------------------------------------------------------------------------------- 1 | module-name: lifecycle-manager 2 | kind: kcp 3 | checkmarx-one: 4 | preset: go-default 5 | exclude: 6 | - "**/test/**" 7 | - "**/*_test.go" 8 | - "**/testutils/**" 9 | - "tests/**" 10 | bdba: 11 | - europe-docker.pkg.dev/kyma-project/prod/lifecycle-manager:1.9.3 12 | - europe-docker.pkg.dev/kyma-project/prod/lifecycle-manager:1.10.1 13 | - europe-docker.pkg.dev/kyma-project/prod/lifecycle-manager:latest 14 | mend: 15 | language: golang-mod 16 | exclude: 17 | - "**/test/**" 18 | - "**/*_test.go" 19 | - "**/testutils/**" 20 | - "tests/**" 21 | -------------------------------------------------------------------------------- /.yamllint.yml: -------------------------------------------------------------------------------- 1 | extends: default 2 | ignore: | 3 | /config/crd/bases/ 4 | /config/samples/tests/crds/ 5 | /config/samples/component-integration-installed/crd/ 6 | lifecycle-manager/config/crd/bases/ 7 | lifecycle-manager/config/samples/tests/crds/ 8 | lifecycle-manager/config/samples/component-integration-installed/crd/ 9 | #Rules config: https://yamllint.readthedocs.io/en/stable/rules.html 10 | rules: 11 | line-length: 12 | max: 225 13 | indentation: 14 | spaces: 2 15 | braces: 16 | max-spaces-inside: 1 17 | max-spaces-inside-empty: 1 18 | brackets: 19 | max-spaces-inside: 1 20 | -------------------------------------------------------------------------------- /api/shared/operator_annotations.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | const ( 4 | FQDN = OperatorGroup + Separator + "fqdn" 5 | 6 | // OwnedByAnnotation defines the resource managing the resource. Differing from ManagedBy 7 | // in that it does not reference controllers. Used by the runtime-watcher to determine the 8 | // corresponding CR in KCP. 9 | OwnedByAnnotation = OperatorGroup + Separator + "owned-by" 10 | OwnedByFormat = "%s/%s" 11 | IsClusterScopedAnnotation = OperatorGroup + Separator + "is-cluster-scoped" 12 | UnmanagedAnnotation = OperatorGroup + Separator + "is-unmanaged" 13 | ) 14 | -------------------------------------------------------------------------------- /config/maintenance_windows/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | generatorOptions: 4 | disableNameSuffixHash: true 5 | configMapGenerator: 6 | - name: maintenance-config 7 | files: 8 | - policy.json 9 | patches: 10 | - path: patches/volume_mount.yaml 11 | target: 12 | kind: Deployment 13 | name: controller-manager 14 | transformers: 15 | - |- 16 | apiVersion: builtin 17 | kind: NamespaceTransformer 18 | metadata: 19 | name: add-maintwindows-cm-to-kcp-system 20 | namespace: kcp-system 21 | unsetOnly: true 22 | -------------------------------------------------------------------------------- /versions.yaml: -------------------------------------------------------------------------------- 1 | # defines the versions of the tools used in the project 2 | certManager: "1.19.2" 3 | gardenerCertManager: "0.18.0" 4 | controllerTools: "0.18.0" 5 | docker: "27.5.1" 6 | go: "1.25.5" 7 | golangciLint: "2.4.0" 8 | istio: "1.26.4" # https://github.com/kyma-project/lifecycle-manager/issues/2797 9 | k3d: "5.8.3" 10 | k8s: "1.32.2" # kubernetes version used in e2e tests 11 | envtest_k8s: "1.32.0" # kubernetes version used in integration tests 12 | kubectl: "1.31.3" 13 | kustomize: "5.4.3" 14 | modulectl: "2.5.0" 15 | ocm-cli: "0.32.0" 16 | template-operator: "1.0.4" 17 | yq: "4.45.1" 18 | envtest: "0.21" 19 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /.github/actions/install-k3d/action.yml: -------------------------------------------------------------------------------- 1 | name: Install k3d 2 | description: Downloads k3d and installs it locally. 3 | inputs: 4 | k3d_version: 5 | description: The version of k3d to install. For example, 5.6.0. 6 | required: true 7 | runs: 8 | using: composite 9 | steps: 10 | - name: Install k3d 11 | shell: bash 12 | run: | 13 | mkdir -p ./k3d/bin 14 | curl -L -s https://github.com/k3d-io/k3d/releases/download/v${{ inputs.k3d_version }}/k3d-linux-amd64 -o k3d-linux 15 | chmod +x k3d-linux 16 | mv k3d-linux ./k3d/bin/k3d 17 | echo "$(pwd)/k3d/bin" >> $GITHUB_PATH 18 | -------------------------------------------------------------------------------- /.github/workflows/lint-yaml.yml: -------------------------------------------------------------------------------- 1 | name: Lint yaml 2 | 3 | permissions: { } 4 | 5 | on: 6 | pull_request: 7 | branches: 8 | - main 9 | - feat/** 10 | workflow_dispatch: 11 | 12 | jobs: 13 | yamllint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout lifecycle-manager 17 | uses: actions/checkout@v4 18 | with: 19 | path: lifecycle-manager 20 | - name: yaml-lint 21 | uses: ibiqlik/action-yamllint@b74a2626a991d676b6ec243a6458ff86cccf2d2d 22 | with: 23 | no_warnings: true 24 | config_file: ./lifecycle-manager/.yamllint.yml 25 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This file provides an overview of code owners in the `lifecycle-manager` repository. 2 | 3 | # Each line is a file pattern followed by one or more owners. 4 | # The last matching pattern has the most precedence. 5 | # For more details read the following article on GitHub: https://help.github.com/articles/about-codeowners/. 6 | 7 | # These are the default owners for the whole content of the `kyma` repository. The default owners are automatically added as reviewers when you open a pull request unless different owners are specified in the file. 8 | 9 | * @kyma-project/jellyfish 10 | *.md @kyma-project/technical-writers 11 | -------------------------------------------------------------------------------- /.github/actions/install-istioctl/action.yml: -------------------------------------------------------------------------------- 1 | name: Install istioctl 2 | description: Downloads istioctl and installs it locally. 3 | inputs: 4 | istio_version: 5 | description: The version of istioctl to install. For example, 1.20.3. 6 | required: true 7 | runs: 8 | using: composite 9 | steps: 10 | - name: Install istioctl 11 | shell: bash 12 | run: | 13 | curl -L https://istio.io/downloadIstio | ISTIO_VERSION=${{ inputs.istio_version }} TARGET_ARCH=x86_64 sh - 14 | chmod +x istio-${{ inputs.istio_version }}/bin/istioctl 15 | echo "$(pwd)/istio-${{ inputs.istio_version }}/bin" >> $GITHUB_PATH 16 | -------------------------------------------------------------------------------- /docs/user/01-10-kyma-crd.md: -------------------------------------------------------------------------------- 1 | # Kyma Custom Resource 2 | 3 | 4 | 5 | The `kymas.operator.kyma-project.io` Custom Resource Definition (CRD) is a comprehensive specification that defines the structure and format used to manage a cluster and its desired state. It contains the list of added modules and their state. 6 | 7 | To get the latest CRD in the YAML format, run the following command: 8 | 9 | ```bash 10 | kubectl get crd kymas.operator.kyma-project.io -o yaml 11 | ``` 12 | 13 | For more information on the fields and how to use them, see [Kyma](../contributor/resources/01-kyma.md). 14 | -------------------------------------------------------------------------------- /internal/rate_limiter.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "time" 5 | 6 | "golang.org/x/time/rate" 7 | "k8s.io/client-go/util/workqueue" 8 | ctrl "sigs.k8s.io/controller-runtime" 9 | ) 10 | 11 | func RateLimiter( 12 | failureBaseDelay time.Duration, failureMaxDelay time.Duration, 13 | frequency int, burst int, 14 | ) workqueue.TypedRateLimiter[ctrl.Request] { 15 | return workqueue.NewTypedMaxOfRateLimiter( 16 | workqueue.NewTypedItemExponentialFailureRateLimiter[ctrl.Request](failureBaseDelay, failureMaxDelay), 17 | &workqueue.TypedBucketRateLimiter[ctrl.Request]{Limiter: rate.NewLimiter(rate.Limit(frequency), burst)}, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /scripts/tests/apply_gcm_certificate_management.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | GARDENER_CERT_MANAGER_VERSION=$1 4 | GARDENER_CERT_MANAGER_RENEWAL_WINDOW=$2 5 | 6 | helm install cert-controller-manager \ 7 | oci://europe-docker.pkg.dev/gardener-project/releases/charts/cert-controller-manager \ 8 | --version v"$GARDENER_CERT_MANAGER_VERSION" \ 9 | --set configuration.renewalWindow="$GARDENER_CERT_MANAGER_RENEWAL_WINDOW" 10 | 11 | # this is needed for GCM to work since this is not included in the helm chart for GCM 12 | kubectl apply -f https://raw.githubusercontent.com/gardener/cert-management/master/examples/11-dns.gardener.cloud_dnsentries.yaml 13 | -------------------------------------------------------------------------------- /.github/workflows/create-release-api.yml: -------------------------------------------------------------------------------- 1 | name: "Create Release For KLM API Module" 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | description: "Semantic version to release (e.g. 1.2.3 or v1.2.3)" 7 | required: true 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | call-create-release: 13 | uses: ./.github/workflows/create-release-go-module.yml 14 | with: 15 | version: ${{ github.event.inputs.version }} 16 | module: github.com/kyma-project/lifecycle-manager/api 17 | module_dir: api 18 | go_version_file: api/go.mod 19 | go_sum_file: api/go.sum 20 | secrets: inherit 21 | -------------------------------------------------------------------------------- /.github/workflows/test-unit.yml: -------------------------------------------------------------------------------- 1 | name: TestSuite integration and unit tests 2 | 3 | permissions: { } 4 | 5 | on: 6 | pull_request: 7 | types: [ opened, synchronize, reopened, ready_for_review ] 8 | 9 | jobs: 10 | envtest-and-unittest: 11 | name: "Run 'make test'" 12 | runs-on: ubuntu-latest 13 | env: 14 | GOSUMDB: sum.golang.org 15 | steps: 16 | - name: Checkout lifecycle-manager 17 | uses: actions/checkout@v4 18 | - name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version-file: 'go.mod' 22 | - name: Run 'make test' 23 | run: | 24 | make test 25 | -------------------------------------------------------------------------------- /.github/actions/export-kubeconfigs/action.yml: -------------------------------------------------------------------------------- 1 | name: Export kubeconfigs 2 | description: Merges the configs from KCP and SKR k3d clusters into the default kubeconfig and exports the same as environment variables KCP_KUBECONFIG and SKR_KUBECONFIG. 3 | inputs: 4 | context_name: 5 | description: The name of the context to use. 6 | required: true 7 | runs: 8 | using: composite 9 | steps: 10 | - name: Export kubeconfigs 11 | shell: bash 12 | run: | 13 | k3d kubeconfig merge -a -d 14 | echo "KCP_KUBECONFIG=$(k3d kubeconfig write kcp)" >> $GITHUB_ENV 15 | echo "SKR_KUBECONFIG=$(k3d kubeconfig write skr)" >> $GITHUB_ENV 16 | -------------------------------------------------------------------------------- /internal/pkg/metrics/shared.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" 6 | ) 7 | 8 | type SharedMetrics struct { 9 | requeueReasonCounter *prometheus.CounterVec 10 | } 11 | 12 | func NewSharedMetrics() *SharedMetrics { 13 | metrics := &SharedMetrics{ 14 | requeueReasonCounter: prometheus.NewCounterVec(prometheus.CounterOpts{ 15 | Name: MetricRequeueReason, 16 | Help: "Indicates the reason for requeue", 17 | }, []string{requeueReasonLabel, requeueTypeLabel}), 18 | } 19 | ctrlmetrics.Registry.MustRegister(metrics.requeueReasonCounter) 20 | return metrics 21 | } 22 | -------------------------------------------------------------------------------- /config/control-plane/patches/unique_deployment_webhook_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: manager 10 | ports: 11 | - containerPort: 9443 12 | name: webhook-server 13 | protocol: TCP 14 | volumeMounts: 15 | - mountPath: /tmp/k8s-webhook-server/serving-certs 16 | name: cert 17 | readOnly: true 18 | volumes: 19 | - name: cert 20 | secret: 21 | defaultMode: 420 22 | secretName: klm-controller-manager-webhook 23 | -------------------------------------------------------------------------------- /config/watcher_local_test/patches/unique_deployment_webhook_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: manager 10 | ports: 11 | - containerPort: 9443 12 | name: webhook-server 13 | protocol: TCP 14 | volumeMounts: 15 | - mountPath: /tmp/k8s-webhook-server/serving-certs 16 | name: cert 17 | readOnly: true 18 | volumes: 19 | - name: cert 20 | secret: 21 | defaultMode: 420 22 | secretName: klm-controller-manager-webhook 23 | -------------------------------------------------------------------------------- /.github/scripts/release/get_release_by_tag.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o nounset 4 | set -o pipefail 5 | 6 | RELEASE_TAG=$1 7 | GITHUB_TOKEN=$2 8 | 9 | GITHUB_URL=https://api.github.com/repos/$CODE_REPOSITORY 10 | GITHUB_AUTH_HEADER="Authorization: Bearer $GITHUB_TOKEN" 11 | 12 | curl -L \ 13 | -s \ 14 | --fail-with-body \ 15 | -H "Accept: application/vnd.github+json" \ 16 | -H "$GITHUB_AUTH_HEADER" \ 17 | -H "X-GitHub-Api-Version: 2022-11-28" \ 18 | "$GITHUB_URL"/releases/tags/"$RELEASE_TAG" 19 | 20 | CURL_EXIT_CODE=$? 21 | 22 | if [[ $CURL_EXIT_CODE == 0 ]]; then 23 | echo "Release with tag: $RELEASE_TAG already exists!" 24 | exit 1 25 | fi 26 | 27 | 28 | -------------------------------------------------------------------------------- /config/watcher_local_test_gcm/patches/unique_deployment_webhook_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: manager 10 | ports: 11 | - containerPort: 9443 12 | name: webhook-server 13 | protocol: TCP 14 | volumeMounts: 15 | - mountPath: /tmp/k8s-webhook-server/serving-certs 16 | name: cert 17 | readOnly: true 18 | volumes: 19 | - name: cert 20 | secret: 21 | defaultMode: 420 22 | secretName: klm-controller-manager-webhook 23 | -------------------------------------------------------------------------------- /config/certmanager/certmanager_role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Give controller-manager permissions to the cert-manager-related resources for watcher 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: Role 5 | metadata: 6 | name: controller-manager-certmanager 7 | namespace: istio-system 8 | rules: 9 | - apiGroups: 10 | - "" 11 | resources: 12 | - secrets 13 | verbs: 14 | - watch 15 | - list 16 | - get 17 | - create 18 | - update 19 | - delete 20 | - apiGroups: 21 | - cert-manager.io 22 | resources: 23 | - certificates 24 | verbs: 25 | - watch 26 | - list 27 | - get 28 | - create 29 | - patch 30 | - delete 31 | -------------------------------------------------------------------------------- /internal/util/collections/collections.go: -------------------------------------------------------------------------------- 1 | package collections 2 | 3 | // MergeMapsSilent merges map2 into map1. 4 | // It does not indicate if map1 changed. 5 | func MergeMapsSilent(map1, map2 map[string]string) map[string]string { 6 | mergedMap, _ := MergeMaps(map1, map2) 7 | return mergedMap 8 | } 9 | 10 | // MergeMaps merges map2 into map1. 11 | // It returns true if map1 changed. 12 | func MergeMaps(map1, map2 map[string]string) (map[string]string, bool) { 13 | changed := false 14 | if map1 == nil { 15 | map1 = make(map[string]string) 16 | } 17 | 18 | for k, v := range map2 { 19 | if map1[k] != v { 20 | map1[k] = v 21 | changed = true 22 | } 23 | } 24 | 25 | return map1, changed 26 | } 27 | -------------------------------------------------------------------------------- /.github/actions/install-modulectl/action.yml: -------------------------------------------------------------------------------- 1 | name: Install modulectl 2 | description: Downloads modulectl and installs it locally. 3 | inputs: 4 | modulectl_version: 5 | description: The version of modulectl to install. For example, 1.0.0. 6 | required: true 7 | runs: 8 | using: composite 9 | steps: 10 | - name: Install modulectl 11 | shell: bash 12 | run: | 13 | mkdir -p ./modulectl/bin 14 | wget https://github.com/kyma-project/modulectl/releases/download/${{ inputs.modulectl_version }}/modulectl-linux -O modulectl-linux 15 | chmod +x modulectl-linux 16 | mv modulectl-linux ./modulectl/bin/modulectl 17 | echo "$(pwd)/modulectl/bin" >> $GITHUB_PATH 18 | -------------------------------------------------------------------------------- /internal/descriptor/cache/key_test.go: -------------------------------------------------------------------------------- 1 | package cache_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | descriptorcache "github.com/kyma-project/lifecycle-manager/internal/descriptor/cache" 9 | "github.com/kyma-project/lifecycle-manager/pkg/testutils" 10 | ) 11 | 12 | func TestGenerateDescriptorKey(t *testing.T) { 13 | t.Run("should generate correct descriptor cache key", func(t *testing.T) { 14 | moduleName := "name" 15 | moduleVersion := "1.0.0" 16 | expected := "name:1.0.0" 17 | 18 | ocmId := testutils.MustNewComponentId(moduleName, moduleVersion) 19 | res := descriptorcache.GenerateDescriptorKey(*ocmId) 20 | assert.Equal(t, expected, string(res)) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /internal/repository/skr/crd/repo_test.go: -------------------------------------------------------------------------------- 1 | package crd_test 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/types" 5 | "sigs.k8s.io/controller-runtime/pkg/client" 6 | 7 | errorsinternal "github.com/kyma-project/lifecycle-manager/internal/errors" 8 | ) 9 | 10 | type skrClientRetrieverStub struct { 11 | client client.Client 12 | 13 | receivedKey types.NamespacedName 14 | } 15 | 16 | func (s *skrClientRetrieverStub) retrieverFunc() func(kymaName types.NamespacedName) (client.Client, error) { 17 | return func(kymaName types.NamespacedName) (client.Client, error) { 18 | s.receivedKey = kymaName 19 | if s.client == nil { 20 | return nil, errorsinternal.ErrSkrClientNotFound 21 | } 22 | return s.client, nil 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /internal/repository/skr/kyma/repo_test.go: -------------------------------------------------------------------------------- 1 | package kyma_test 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/types" 5 | "sigs.k8s.io/controller-runtime/pkg/client" 6 | 7 | errorsinternal "github.com/kyma-project/lifecycle-manager/internal/errors" 8 | ) 9 | 10 | type skrClientRetrieverStub struct { 11 | client client.Client 12 | 13 | receivedKey types.NamespacedName 14 | } 15 | 16 | func (s *skrClientRetrieverStub) retrieverFunc() func(kymaName types.NamespacedName) (client.Client, error) { 17 | return func(kymaName types.NamespacedName) (client.Client, error) { 18 | s.receivedKey = kymaName 19 | if s.client == nil { 20 | return nil, errorsinternal.ErrSkrClientNotFound 21 | } 22 | return s.client, nil 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/create-release-maintenancewindows.yml: -------------------------------------------------------------------------------- 1 | name: "Create Release For Maintenancewindows Module" 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | description: "Semantic version to release (e.g. 1.2.3 or v1.2.3)" 7 | required: true 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | call-create-release: 13 | uses: ./.github/workflows/create-release-go-module.yml 14 | with: 15 | version: ${{ github.event.inputs.version }} 16 | module: github.com/kyma-project/lifecycle-manager/maintenancewindows 17 | module_dir: maintenancewindows 18 | go_version_file: maintenancewindows/go.mod 19 | go_sum_file: maintenancewindows/go.sum 20 | secrets: inherit 21 | -------------------------------------------------------------------------------- /docs/contributor/resources/04-watcher.md: -------------------------------------------------------------------------------- 1 | # Watcher 2 | 3 | The `watchers.operator.kyma-project.io` Custom Resource Definition (CRD) defines the structure and format used to configure the Watcher resource. 4 | 5 | The Watcher custom resource (CR) defines the callback functionality for synchronized Kyma runtime clusters, that allows lower latencies before the Kyma Control Plane cluster detects any changes. 6 | 7 | For more information on the Watcher CR, see [Watcher CR](https://github.com/kyma-project/runtime-watcher/blob/main/docs/api.md) in the `/runtime-watcher` repository. 8 | 9 | To get the latest CRD in the YAML format, run the following command: 10 | 11 | ```bash 12 | kubectl get crd watchers.operator.kyma-project.io -o yaml 13 | ``` 14 | -------------------------------------------------------------------------------- /internal/repository/skr/kyma/status/repo_test.go: -------------------------------------------------------------------------------- 1 | package status_test 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/types" 5 | "sigs.k8s.io/controller-runtime/pkg/client" 6 | 7 | errorsinternal "github.com/kyma-project/lifecycle-manager/internal/errors" 8 | ) 9 | 10 | type skrClientRetrieverStub struct { 11 | client client.Client 12 | 13 | receivedKey types.NamespacedName 14 | } 15 | 16 | func (s *skrClientRetrieverStub) retrieverFunc() func(kymaName types.NamespacedName) (client.Client, error) { 17 | return func(kymaName types.NamespacedName) (client.Client, error) { 18 | s.receivedKey = kymaName 19 | if s.client == nil { 20 | return nil, errorsinternal.ErrSkrClientNotFound 21 | } 22 | return s.client, nil 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/util/error_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/kyma-project/lifecycle-manager/pkg/util" 9 | ) 10 | 11 | func TestIsTLSCertExpiredError(t *testing.T) { 12 | errorMessageFromLogs := "couldn't get current server API group list: " + 13 | "Get \"https://api.somehost.dev.kyma.com/api?timeout=32s\": remote error: tls: expired certificate" 14 | assert.True(t, util.IsTLSCertExpiredError(errorMessageFromLogs)) 15 | } 16 | 17 | func TestIsUnauthorizedError(t *testing.T) { 18 | errorMessageFromLogs := "patch for clusterrolebindings/template-operator-proxy-rolebinding failed: Unauthorized\n" 19 | assert.True(t, util.IsUnauthorizedError(errorMessageFromLogs)) 20 | } 21 | -------------------------------------------------------------------------------- /maintenancewindows/resolver/resolver_test.go: -------------------------------------------------------------------------------- 1 | package resolver_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/kyma-project/lifecycle-manager/maintenancewindows/resolver" 10 | ) 11 | 12 | func TestGetMaintenancePolicyPool(t *testing.T) { 13 | t.Setenv(resolver.PolicyPathENV, "./testdata") 14 | 15 | pool, err := resolver.GetMaintenancePolicyPool() 16 | require.NoError(t, err) 17 | 18 | assert.Len(t, pool, 2) 19 | assert.Contains(t, pool, "ruleset-1.json") 20 | assert.Contains(t, pool, "ruleset-2.json") 21 | 22 | data1 := pool["ruleset-1.json"] 23 | data2 := pool["ruleset-2.json"] 24 | assert.NotNil(t, data1) 25 | assert.NotNil(t, data2) 26 | } 27 | -------------------------------------------------------------------------------- /tests/integration/controller/kcp/controller_test.go: -------------------------------------------------------------------------------- 1 | package kcp_test 2 | 3 | import ( 4 | "github.com/kyma-project/lifecycle-manager/api/shared" 5 | 6 | . "github.com/kyma-project/lifecycle-manager/pkg/testutils" 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | var _ = Describe("Kyma with managed fields in kcp mode", Ordered, func() { 12 | kyma := NewTestKyma("managed-kyma") 13 | 14 | registerControlPlaneLifecycleForKyma(kyma) 15 | 16 | It("Should result in a managed field with manager named 'lifecycle-manager'", func() { 17 | Eventually(ContainsKymaManagerField, Timeout, Interval). 18 | WithArguments(ctx, kcpClient, kyma.GetName(), kyma.GetNamespace(), shared.OperatorName). 19 | Should(BeTrue()) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /.github/pull-request-template.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | **Description** 9 | 10 | Changes proposed in this pull request: 11 | 12 | - ... 13 | - ... 14 | - ... 15 | 16 | **Related issue(s)** 17 | 18 | -------------------------------------------------------------------------------- /config/gardener-certmanager/gardener_certmanager_role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Give controller-manager permissions to the cert-manager-related resources for watcher 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: Role 5 | metadata: 6 | name: controller-manager-gardener-certmanager 7 | namespace: istio-system 8 | rules: 9 | - apiGroups: 10 | - "" 11 | resources: 12 | - secrets 13 | verbs: 14 | - watch 15 | - list 16 | - get 17 | - create 18 | - update 19 | - delete 20 | - apiGroups: 21 | - cert.gardener.cloud 22 | resources: 23 | - certificates 24 | verbs: 25 | - watch 26 | - list 27 | - get 28 | - create 29 | - patch 30 | - update 31 | - delete 32 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: controller-manager-leader-election 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - coordination.k8s.io 21 | resources: 22 | - leases 23 | verbs: 24 | - get 25 | - list 26 | - watch 27 | - create 28 | - update 29 | - patch 30 | - delete 31 | - apiGroups: 32 | - "" 33 | resources: 34 | - events 35 | verbs: 36 | - create 37 | - patch 38 | -------------------------------------------------------------------------------- /internal/controller/watcherEventAdapter.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/kyma-project/runtime-watcher/listener/pkg/v2/types" 5 | "sigs.k8s.io/controller-runtime/pkg/event" 6 | ) 7 | 8 | type ( 9 | WatcherListenerEvent = types.GenericEvent 10 | CtrlRuntimeEvent = event.GenericEvent 11 | ) 12 | 13 | // AdaptEvents converts given channel from the type used by runtime-watcher/listener 14 | // module to the type required by the controller-runtime library. 15 | func AdaptEvents(listenerChan func() <-chan WatcherListenerEvent) <-chan CtrlRuntimeEvent { 16 | dest := make(chan CtrlRuntimeEvent) 17 | go func() { 18 | defer close(dest) 19 | for evt := range listenerChan() { 20 | dest <- CtrlRuntimeEvent{Object: evt.Object} 21 | } 22 | }() 23 | return dest 24 | } 25 | -------------------------------------------------------------------------------- /internal/descriptor/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/kyma-project/lifecycle-manager/internal/descriptor/types" 7 | ) 8 | 9 | type DescriptorCache struct { 10 | cache sync.Map 11 | } 12 | 13 | func NewDescriptorCache() *DescriptorCache { 14 | return &DescriptorCache{ 15 | cache: sync.Map{}, 16 | } 17 | } 18 | 19 | func (d *DescriptorCache) Get(key DescriptorKey) *types.Descriptor { 20 | value, ok := d.cache.Load(key) 21 | if !ok { 22 | return nil 23 | } 24 | desc, ok := value.(*types.Descriptor) 25 | if !ok { 26 | return nil 27 | } 28 | 29 | return &types.Descriptor{ComponentDescriptor: desc.Copy()} 30 | } 31 | 32 | func (d *DescriptorCache) Set(key DescriptorKey, value *types.Descriptor) { 33 | d.cache.Store(key, value) 34 | } 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation-improvement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation improvement 3 | about: Suggest an improvement or report a bug in the documentation 4 | --- 5 | 6 | 11 | 12 | **Description** 13 | 14 | 15 | 16 | **Reasons** 17 | 18 | 19 | 20 | **Assignees** 21 | 22 | @kyma-project/technical-writers 23 | 24 | **Attachments** 25 | 26 | 27 | -------------------------------------------------------------------------------- /internal/repository/watcher/certificate/common.go: -------------------------------------------------------------------------------- 1 | package certificate 2 | 3 | import ( 4 | "errors" 5 | 6 | k8slabels "k8s.io/apimachinery/pkg/labels" 7 | 8 | "github.com/kyma-project/lifecycle-manager/api/shared" 9 | ) 10 | 11 | const ( 12 | DefaultOrganizationalUnit = "BTP Kyma Runtime" 13 | DefaultOrganization = "SAP SE" 14 | DefaultLocality = "Walldorf" 15 | DefaultProvince = "Baden-Württemberg" 16 | DefaultCountry = "DE" 17 | ) 18 | 19 | var ErrNoRenewalTime = errors.New("no renewal time set for certificate") 20 | 21 | // GetCertificateLabels returns purpose and managed-by labels. 22 | func GetCertificateLabels() k8slabels.Set { 23 | return k8slabels.Set{ 24 | shared.PurposeLabel: shared.CertManager, 25 | shared.ManagedBy: shared.OperatorName, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/epic.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Epic 3 | about: Suggest a long-term or complex improvement to the project 4 | --- 5 | 6 | 10 | 11 | **Description** 12 | 13 | 14 | 15 | **Acceptance Criteria** 16 | 17 | 18 | 19 | - [ ] 20 | - [ ] 21 | - [ ] 22 | - [ ] 23 | - [ ] 24 | 25 | **Reasons** 26 | 27 | 28 | 29 | **Attachments** 30 | 31 | 32 | -------------------------------------------------------------------------------- /internal/controller/purge/setup.go: -------------------------------------------------------------------------------- 1 | package purge 2 | 3 | import ( 4 | "fmt" 5 | 6 | ctrl "sigs.k8s.io/controller-runtime" 7 | ctrlruntime "sigs.k8s.io/controller-runtime/pkg/controller" 8 | "sigs.k8s.io/controller-runtime/pkg/predicate" 9 | 10 | "github.com/kyma-project/lifecycle-manager/api/v1beta2" 11 | ) 12 | 13 | const controllerName = "purge" 14 | 15 | func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts ctrlruntime.Options) error { 16 | if err := ctrl.NewControllerManagedBy(mgr). 17 | For(&v1beta2.Kyma{}). 18 | Named(controllerName). 19 | WithOptions(opts). 20 | WithEventFilter(predicate.Or(predicate.GenerationChangedPredicate{}, predicate.LabelChangedPredicate{})). 21 | Complete(r); err != nil { 22 | return fmt.Errorf("failed to setup manager for purge controller: %w", err) 23 | } 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /api/shared/resource.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "strings" 5 | 6 | apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | "k8s.io/apimachinery/pkg/runtime/schema" 9 | ) 10 | 11 | // +k8s:deepcopy-gen=true 12 | type Resource struct { 13 | apimetav1.GroupVersionKind `json:",inline"` 14 | 15 | Name string `json:"name"` 16 | Namespace string `json:"namespace"` 17 | } 18 | 19 | func (r Resource) ToUnstructured() *unstructured.Unstructured { 20 | obj := unstructured.Unstructured{} 21 | obj.SetGroupVersionKind(schema.GroupVersionKind(r.GroupVersionKind)) 22 | obj.SetName(r.Name) 23 | obj.SetNamespace(r.Namespace) 24 | return &obj 25 | } 26 | 27 | func (r Resource) ID() string { 28 | return strings.Join([]string{r.Namespace, r.Name, r.Group, r.Version, r.Kind}, "/") 29 | } 30 | -------------------------------------------------------------------------------- /config/gardener-certmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | resources: 4 | - certificate_webhook.yaml 5 | - certificate_watcher.yaml 6 | - gardener_certmanager_role.yaml 7 | - gardener_certmanager_role_binding.yaml 8 | configurations: 9 | - kustomizeconfig.yaml 10 | transformers: 11 | - |- 12 | apiVersion: builtin 13 | kind: PatchTransformer 14 | metadata: 15 | name: fix-cert-dns-names 16 | patch: '[{"op": "replace", "path": "/spec/dnsNames/0", "value": "klm-webhook-service.kcp-system.svc"}, {"op": "replace", "path": "/spec/dnsNames/1", "value": "klm-webhook-service.kcp-system.svc.cluster.local"}]' 17 | target: 18 | kind: Certificate 19 | name: controller-manager-webhook-serving 20 | version: v1alpha1 21 | group: cert.gardener.cloud 22 | -------------------------------------------------------------------------------- /internal/manifest/filemutex/cache.go: -------------------------------------------------------------------------------- 1 | package filemutex 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | ) 7 | 8 | var ErrMutexConversion = errors.New("failed to convert cached value to mutex") 9 | 10 | type MutexCache struct { 11 | cache *sync.Map 12 | } 13 | 14 | func NewMutexCache(concurrMap *sync.Map) *MutexCache { 15 | if concurrMap == nil { 16 | return &MutexCache{cache: &sync.Map{}} 17 | } 18 | return &MutexCache{cache: concurrMap} 19 | } 20 | 21 | // GetLocker always returns the same sync.Locker instance for a given key. 22 | func (m *MutexCache) GetLocker(key string) (sync.Locker, error) { 23 | val, ok := m.cache.Load(key) 24 | if !ok { 25 | val, _ = m.cache.LoadOrStore(key, &sync.Mutex{}) 26 | } 27 | 28 | mutex, ok := val.(*sync.Mutex) 29 | if !ok { 30 | return nil, ErrMutexConversion 31 | } 32 | return mutex, nil 33 | } 34 | -------------------------------------------------------------------------------- /pkg/matcher/matcher_test.go: -------------------------------------------------------------------------------- 1 | package matcher_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/kyma-project/lifecycle-manager/pkg/matcher" 9 | "github.com/kyma-project/lifecycle-manager/pkg/testutils/builder" 10 | ) 11 | 12 | func TestCreateCRDMatcherFrom(t *testing.T) { 13 | t.Parallel() 14 | names := "kymas.operator.kyma-project.io,manifest.operator.kyma-project.io" 15 | matcherFunc := matcher.CreateCRDMatcherFrom(names) 16 | 17 | crdBuilder := builder.NewCRDBuilder() 18 | kymaCrd := crdBuilder.WithName("kyma").Build() 19 | manifestCrd := crdBuilder.WithName("manifest").Build() 20 | watcherCrd := crdBuilder.WithName("watcher").Build() 21 | 22 | require.True(t, matcherFunc(kymaCrd)) 23 | require.True(t, matcherFunc(manifestCrd)) 24 | require.False(t, matcherFunc(watcherCrd)) 25 | } 26 | -------------------------------------------------------------------------------- /scripts/tests/deploy_mandatory_modulereleasemeta.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o nounset 3 | set -o errexit 4 | set -E 5 | set -o pipefail 6 | 7 | MODULE_NAME=$1 8 | VERSION=$2 9 | 10 | cat < module-release-meta-mandatory.yaml 11 | apiVersion: operator.kyma-project.io/v1beta2 12 | kind: ModuleReleaseMeta 13 | metadata: 14 | name: ${MODULE_NAME} 15 | namespace: kcp-system 16 | spec: 17 | moduleName: ${MODULE_NAME} 18 | ocmComponentName: kyma-project.io/module/${MODULE_NAME} 19 | mandatory: 20 | version: ${VERSION} 21 | EOF 22 | 23 | kubectl apply -f module-release-meta-mandatory.yaml 24 | 25 | echo "Mandatory ModuleReleaseMeta created successfully" 26 | rm -f module-release-meta-mandatory.yaml 27 | 28 | kubectl get modulereleasemeta "${MODULE_NAME}" -n kcp-system -o yaml 29 | kubectl get moduletemplate -n kcp-system -o wide 30 | -------------------------------------------------------------------------------- /scripts/tests/ocm-config-private-registry.yaml: -------------------------------------------------------------------------------- 1 | type: generic.config.ocm.software/v1 2 | configurations: 3 | - type: ocm.config.ocm.software 4 | aliases: 5 | k3d-private-oci-reg.localhost:5001: 6 | type: OCIRegistry 7 | baseUrl: http://k3d-private-oci-reg.localhost:5001 8 | - type: oci.config.ocm.software 9 | aliases: 10 | k3d-private-oci-reg.localhost:5001: 11 | type: OCIRegistry 12 | baseUrl: http://k3d-private-oci-reg.localhost:5001 13 | - type: credentials.config.ocm.software 14 | consumers: 15 | - identity: 16 | type: OCIRegistry 17 | hostname: k3d-private-oci-reg.localhost 18 | port: "5001" 19 | credentials: 20 | - type: Credentials 21 | properties: 22 | username: myuser 23 | password: mypass 24 | -------------------------------------------------------------------------------- /api/v1beta1/resource.go: -------------------------------------------------------------------------------- 1 | package v1beta1 2 | 3 | import ( 4 | "strings" 5 | 6 | apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | "k8s.io/apimachinery/pkg/runtime/schema" 9 | ) 10 | 11 | // +k8s:deepcopy-gen=true 12 | type Resource struct { 13 | apimetav1.GroupVersionKind `json:",inline"` 14 | 15 | Name string `json:"name"` 16 | Namespace string `json:"namespace"` 17 | } 18 | 19 | func (r Resource) ToUnstructured() *unstructured.Unstructured { 20 | obj := unstructured.Unstructured{} 21 | obj.SetGroupVersionKind(schema.GroupVersionKind(r.GroupVersionKind)) 22 | obj.SetName(r.Name) 23 | obj.SetNamespace(r.Namespace) 24 | return &obj 25 | } 26 | 27 | func (r Resource) ID() string { 28 | return strings.Join([]string{r.Namespace, r.Name, r.Group, r.Version, r.Kind}, "/") 29 | } 30 | -------------------------------------------------------------------------------- /pkg/testutils/namespace.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "context" 5 | 6 | apicorev1 "k8s.io/api/core/v1" 7 | apierrors "k8s.io/apimachinery/pkg/api/errors" 8 | apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | 11 | "github.com/kyma-project/lifecycle-manager/api/shared" 12 | ) 13 | 14 | const ( 15 | RemoteNamespace = shared.DefaultRemoteNamespace 16 | ControlPlaneNamespace = "kcp-system" 17 | IstioNamespace = "istio-system" 18 | ) 19 | 20 | func CreateNamespace(ctx context.Context, clnt client.Client, name string) error { 21 | namespace := &apicorev1.Namespace{ 22 | ObjectMeta: apimetav1.ObjectMeta{ 23 | Name: name, 24 | }, 25 | } 26 | err := clnt.Create(ctx, namespace) 27 | if !apierrors.IsAlreadyExists(err) { 28 | return err 29 | } 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /internal/gatewaysecret/handler.go: -------------------------------------------------------------------------------- 1 | package gatewaysecret 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | apicorev1 "k8s.io/api/core/v1" 8 | ) 9 | 10 | const ( 11 | TLSCrt = "tls.crt" 12 | TLSKey = "tls.key" 13 | CACrt = "ca.crt" 14 | SecretKind = "Secret" 15 | ) 16 | 17 | type Client interface { 18 | GetWatcherServingCertValidity(ctx context.Context) (time.Time, time.Time, error) 19 | GetGatewaySecret(ctx context.Context) (*apicorev1.Secret, error) 20 | CreateGatewaySecret(ctx context.Context, secret *apicorev1.Secret) error 21 | UpdateGatewaySecret(ctx context.Context, secret *apicorev1.Secret) error 22 | } 23 | 24 | type TimeParserFunc func(secret *apicorev1.Secret, annotation string) (time.Time, error) 25 | 26 | type Handler interface { 27 | ManageGatewaySecret(ctx context.Context, rootSecret *apicorev1.Secret) error 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/lint-markdown-links.yml: -------------------------------------------------------------------------------- 1 | name: Lint Markdown Links 2 | run-name: ${{github.event.pull_request.title}} 3 | 4 | permissions: { } 5 | 6 | on: 7 | pull_request: 8 | schedule: 9 | # Run every day at 5:00 AM 10 | - cron: "0 5 * * *" 11 | 12 | jobs: 13 | markdown-link-check: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | # Relates to version 1.0.15. Sha is used as this is this is the allow-listed value 18 | - uses: gaurav-nelson/github-action-markdown-link-check@d53a906aa6b22b8979d33bc86170567e619495ec 19 | with: 20 | use-quiet-mode: 'yes' 21 | use-verbose-mode: 'no' 22 | config-file: '.mlc.config.json' 23 | folder-path: '.' 24 | max-depth: -1 25 | check-modified-files-only: 'no' 26 | base-branch: 'main' 27 | -------------------------------------------------------------------------------- /.github/workflows/lint-conventional-prs.yml: -------------------------------------------------------------------------------- 1 | name: Lint PR Title 2 | run-name: ${{github.event.pull_request.title}} 3 | 4 | permissions: { } 5 | 6 | on: 7 | pull_request: 8 | types: 9 | - opened 10 | - reopened 11 | - edited 12 | - synchronize 13 | 14 | jobs: 15 | check: 16 | name: Check Title 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | with: 23 | types: | 24 | deps 25 | chore 26 | docs 27 | feat 28 | fix 29 | refactor 30 | test 31 | requireScope: false 32 | # https://regex101.com/r/YybDgS/1 33 | subjectPattern: ^([A-Z].*[^.]|bump .*)$ 34 | -------------------------------------------------------------------------------- /internal/repository/istiogateway/istio_gateway.go: -------------------------------------------------------------------------------- 1 | package istiogateway 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | istioclientapiv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" 8 | "sigs.k8s.io/controller-runtime/pkg/client" 9 | ) 10 | 11 | type Repository struct { 12 | reader client.Reader 13 | } 14 | 15 | func NewRepository(reader client.Reader) *Repository { 16 | return &Repository{ 17 | reader: reader, 18 | } 19 | } 20 | 21 | func (r Repository) Get(ctx context.Context, name, namespace string) (*istioclientapiv1beta1.Gateway, error) { 22 | gateway := &istioclientapiv1beta1.Gateway{} 23 | if err := r.reader.Get(ctx, client.ObjectKey{ 24 | Namespace: namespace, 25 | Name: name, 26 | }, gateway); err != nil { 27 | return nil, fmt.Errorf("failed to get istio gateway %s/%s: %w", namespace, name, err) 28 | } 29 | return gateway, nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/service/skrclient/cache/client_cache_test.go: -------------------------------------------------------------------------------- 1 | package cache_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | skrclientcache "github.com/kyma-project/lifecycle-manager/internal/service/skrclient/cache" 9 | ) 10 | 11 | func TestService_Basic(t *testing.T) { 12 | svc := skrclientcache.NewService() 13 | 14 | require.Equal(t, 0, svc.Size(), "expected size 0") 15 | 16 | svc.AddClient("a", nil) 17 | require.Equal(t, 1, svc.Size(), "expected size 1 after add") 18 | 19 | require.Nil(t, svc.GetClient("a"), "expected nil client value") 20 | 21 | svc.AddClient("b", nil) 22 | require.Equal(t, 2, svc.Size(), "expected size 2 after second add") 23 | 24 | svc.DeleteClient("a") 25 | require.Equal(t, 1, svc.Size(), "expected size 1 after delete") 26 | 27 | require.Nil(t, svc.GetClient("a"), "expected nil for deleted key") 28 | } 29 | -------------------------------------------------------------------------------- /.github/actions/setup-test-clusters/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup test clusters 2 | description: Creates and configures the KCP and SKR clusters. 3 | inputs: 4 | k8s_version: 5 | description: The version of k8s to use. For example, 1.28.7 6 | required: true 7 | cert_manager_version: 8 | description: The version of cert-manager to depoy. For example, 1.13.3. 9 | required: true 10 | runs: 11 | using: composite 12 | steps: 13 | - name: create-test-clusters 14 | shell: bash 15 | run: | 16 | ./lifecycle-manager/scripts/tests/create_test_clusters.sh --k8s-version ${{ inputs.k8s_version }} --cert-manager-version ${{ inputs.cert_manager_version }} 17 | 18 | - uses: ./lifecycle-manager/.github/actions/export-kubeconfigs 19 | 20 | - uses: ./lifecycle-manager/.github/actions/switch-kubectl-context 21 | with: 22 | context_name: k3d-kcp 23 | -------------------------------------------------------------------------------- /tests/integration/commontestutils/skrcontextimpl/accessmanager.go: -------------------------------------------------------------------------------- 1 | package skrcontextimpl 2 | 3 | import ( 4 | "context" 5 | 6 | "k8s.io/client-go/rest" 7 | "sigs.k8s.io/controller-runtime/pkg/envtest" 8 | ) 9 | 10 | type FakeAccessManagerService struct { 11 | skrEnv *envtest.Environment 12 | baseConfig *rest.Config 13 | } 14 | 15 | func NewFakeAccessManagerService(skrEnv *envtest.Environment, baseConfig *rest.Config) *FakeAccessManagerService { 16 | return &FakeAccessManagerService{ 17 | skrEnv: skrEnv, 18 | baseConfig: baseConfig, 19 | } 20 | } 21 | 22 | func (f *FakeAccessManagerService) GetAccessRestConfigByKyma(_ context.Context, _ string) (*rest.Config, error) { 23 | authUser, err := f.skrEnv.AddUser( 24 | envtest.User{ 25 | Name: "skr-admin-account", 26 | Groups: []string{"system:masters"}, 27 | }, f.baseConfig, 28 | ) 29 | return authUser.Config(), err 30 | } 31 | -------------------------------------------------------------------------------- /scripts/tests/deploy_klm_from_sources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Changing current directory to the root of the project 4 | cd $(git rev-parse --show-toplevel) 5 | 6 | # Export necessary environment variables 7 | export KUBECONFIG=${HOME}/.k3d/kcp-local.yaml 8 | export CLUSTER_REGISTRY="k3d-kcp-registry.localhost:5000" 9 | export LOCAL_IMG="localhost:5111/lifecycle-manager" 10 | export CLUSTER_IMG="${CLUSTER_REGISTRY}/lifecycle-manager" 11 | export TAG=$(date +%Y%m%d%H%M%S) 12 | 13 | pushd config/manager 14 | kustomize edit add patch --kind Deployment --patch $echo\ 15 | "- op: add 16 | path: /spec/template/spec/containers/0/args/- 17 | value: --oci-registry-host=http://${CLUSTER_REGISTRY}" 18 | popd 19 | 20 | make docker-build IMG=${LOCAL_IMG}:${TAG} 21 | make docker-push IMG=${LOCAL_IMG}:${TAG} 22 | make local-deploy-with-watcher IMG=${CLUSTER_IMG}:${TAG} 23 | 24 | git restore config/manager/kustomization.yaml 25 | -------------------------------------------------------------------------------- /internal/crd/cache.go: -------------------------------------------------------------------------------- 1 | package crd 2 | 3 | import ( 4 | "sync" 5 | 6 | apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 7 | ) 8 | 9 | type Cache struct { 10 | cache *sync.Map 11 | } 12 | 13 | func NewCache(internalCache *sync.Map) *Cache { 14 | if internalCache == nil { 15 | return &Cache{cache: &sync.Map{}} 16 | } 17 | 18 | return &Cache{cache: internalCache} 19 | } 20 | 21 | func (c *Cache) Get(key string) (apiextensionsv1.CustomResourceDefinition, bool) { 22 | value, ok := c.cache.Load(key) 23 | if !ok { 24 | return apiextensionsv1.CustomResourceDefinition{}, false 25 | } 26 | crd, ok := value.(apiextensionsv1.CustomResourceDefinition) 27 | if !ok { 28 | return apiextensionsv1.CustomResourceDefinition{}, false 29 | } 30 | 31 | return crd, true 32 | } 33 | 34 | func (c *Cache) Add(key string, value apiextensionsv1.CustomResourceDefinition) { 35 | c.cache.Store(key, value) 36 | } 37 | -------------------------------------------------------------------------------- /maintenancewindows/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 6 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /.github/scripts/release/draft_release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o nounset 4 | set -o errexit 5 | set -E 6 | set -o pipefail 7 | 8 | RELEASE_TAG=$1 9 | 10 | GITHUB_URL=https://api.github.com/repos/${CODE_REPOSITORY} 11 | GITHUB_AUTH_HEADER="Authorization: Bearer ${GITHUB_TOKEN}" 12 | CHANGELOG_FILE=$(cat CHANGELOG.md) 13 | 14 | JSON_PAYLOAD=$(jq -n \ 15 | --arg tag_name "$RELEASE_TAG" \ 16 | --arg name "$RELEASE_TAG" \ 17 | --arg body "$CHANGELOG_FILE" \ 18 | '{ 19 | "tag_name": $tag_name, 20 | "name": $name, 21 | "body": $body, 22 | "draft": true 23 | }') 24 | 25 | CURL_RESPONSE=$(curl -L \ 26 | -X POST \ 27 | -H "Accept: application/vnd.github+json" \ 28 | -H "${GITHUB_AUTH_HEADER}" \ 29 | -H "X-GitHub-Api-Version: 2022-11-28" \ 30 | "${GITHUB_URL}"/releases \ 31 | -d "$JSON_PAYLOAD") 32 | 33 | # return the id of the release draft 34 | echo "$CURL_RESPONSE" | jq -r ".id" 35 | 36 | 37 | -------------------------------------------------------------------------------- /.run/Install CRDs.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | -------------------------------------------------------------------------------- /internal/service/mandatorymodule/deletion/usecases/skip_non_deleting.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyma-project/lifecycle-manager/api/v1beta2" 7 | "github.com/kyma-project/lifecycle-manager/internal/errors/mandatorymodule/deletion" 8 | ) 9 | 10 | // SkipNonDeleting is a use case that skips ModuleReleaseMetas that are not in deleting state. 11 | type SkipNonDeleting struct{} 12 | 13 | func NewSkipNonDeleting() *SkipNonDeleting { 14 | return &SkipNonDeleting{} 15 | } 16 | 17 | // IsApplicable returns true if the ModuleReleaseMeta is not in deleting state, it should be skipped. 18 | func (s *SkipNonDeleting) IsApplicable(_ context.Context, mrm *v1beta2.ModuleReleaseMeta) (bool, error) { 19 | return mrm.DeletionTimestamp.IsZero(), nil 20 | } 21 | 22 | func (s *SkipNonDeleting) Execute(_ context.Context, _ *v1beta2.ModuleReleaseMeta) error { 23 | return deletion.ErrMrmNotInDeletingState 24 | } 25 | -------------------------------------------------------------------------------- /.run/Un-Deploy kyma.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | -------------------------------------------------------------------------------- /cmd/composition/service/componentdescriptor/componentdescriptor.go: -------------------------------------------------------------------------------- 1 | package componentdescriptor 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/go-logr/logr" 7 | 8 | "github.com/kyma-project/lifecycle-manager/internal/service/componentdescriptor" 9 | ) 10 | 11 | // ComposeComponentDescriptorService manges creation of a new instance of the ComponentDescriptor Service. 12 | func ComposeComponentDescriptorService( 13 | repository componentdescriptor.OCIRepository, 14 | logger logr.Logger, 15 | bootstrapFailedExitCode int, 16 | ) *componentdescriptor.Service { 17 | tarExtractor := componentdescriptor.NewTarExtractor() 18 | fileExtractor := componentdescriptor.NewFileExtractor(tarExtractor) 19 | 20 | service, err := componentdescriptor.NewService(repository, fileExtractor) 21 | if err != nil { 22 | logger.Error(err, "failed to create OCM descriptor service") 23 | os.Exit(bootstrapFailedExitCode) 24 | } 25 | 26 | return service 27 | } 28 | -------------------------------------------------------------------------------- /internal/manifest/status/state.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "github.com/kyma-project/lifecycle-manager/api/shared" 5 | "github.com/kyma-project/lifecycle-manager/api/v1beta2" 6 | ) 7 | 8 | const ( 9 | ResourcesAreReadyMsg = "resources are ready" 10 | WaitingForResourcesMsg = "waiting for resources to become ready" 11 | ) 12 | 13 | func RequireManifestStateUpdateAfterSyncResource(manifest *v1beta2.Manifest, newState shared.State) bool { 14 | manifestStatus := manifest.GetStatus() 15 | 16 | if newState != manifestStatus.State { 17 | if newState == shared.StateProcessing || newState == shared.StateError { 18 | manifest.SetStatus(manifestStatus.WithState(newState).WithOperation(WaitingForResourcesMsg)) 19 | } else { 20 | ConfirmInstallationCondition(manifest) 21 | manifest.SetStatus(manifestStatus.WithState(newState).WithOperation(ResourcesAreReadyMsg)) 22 | } 23 | return true 24 | } 25 | 26 | return false 27 | } 28 | -------------------------------------------------------------------------------- /.run/Delete Test Clusters.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | -------------------------------------------------------------------------------- /config/webhook/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # the following config is for teaching kustomize where to look at when substituting vars. 2 | # It requires kustomize v2.1.0 or newer to work properly. 3 | nameReference: 4 | - kind: Service 5 | version: v1 6 | fieldSpecs: 7 | - kind: MutatingWebhookConfiguration 8 | group: admissionregistration.k8s.io 9 | path: webhooks/clientConfig/service/name 10 | - kind: ValidatingWebhookConfiguration 11 | group: admissionregistration.k8s.io 12 | path: webhooks/clientConfig/service/name 13 | namespace: 14 | - kind: MutatingWebhookConfiguration 15 | group: admissionregistration.k8s.io 16 | path: webhooks/clientConfig/service/namespace 17 | create: true 18 | - kind: ValidatingWebhookConfiguration 19 | group: admissionregistration.k8s.io 20 | path: webhooks/clientConfig/service/namespace 21 | create: true 22 | varReference: 23 | - path: metadata/annotations 24 | -------------------------------------------------------------------------------- /config/watcher/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | generatorOptions: 4 | disableNameSuffixHash: true 5 | resources: 6 | - gateway.yaml 7 | - kyma_watcher.yaml 8 | patches: 9 | - target: 10 | kind: Deployment 11 | patch: |- 12 | - op: add 13 | path: /spec/template/spec/containers/0/args/- 14 | value: --enable-kcp-watcher 15 | - op: add 16 | path: /spec/template/spec/containers/0/args/- 17 | value: --skr-watcher-path=/skr-webhook 18 | - op: add 19 | path: /spec/template/spec/containers/0/args/- 20 | value: --skr-watcher-image-tag=2.0.9 21 | - op: add 22 | path: /spec/template/spec/containers/0/args/- 23 | value: --skr-watcher-image-registry=europe-docker.pkg.dev/kyma-project/prod 24 | - op: add 25 | path: /spec/template/spec/containers/0/args/- 26 | value: --enable-domain-name-pinning=true 27 | -------------------------------------------------------------------------------- /.run/Deploy KLM from sources.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | -------------------------------------------------------------------------------- /internal/maintenancewindows/testdata/policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": [ 3 | { 4 | "match": { 5 | "plan": "trial|free" 6 | }, 7 | "windows": [ 8 | { 9 | "days": [ 10 | "Mon", 11 | "Tue", 12 | "Wed", 13 | "Thu", 14 | "Fri", 15 | "Sat", 16 | "Sun" 17 | ], 18 | "begin": "01:00:00+00:00", 19 | "end": "01:00:00+00:00" 20 | } 21 | ] 22 | }, 23 | { 24 | "match": { 25 | "region": "europe|eu-|uksouth" 26 | }, 27 | "windows": [ 28 | { 29 | "days": [ 30 | "Sat" 31 | ], 32 | "begin": "21:00:00+00:00", 33 | "end": "00:00:00+00:00" 34 | } 35 | ] 36 | } 37 | ], 38 | "default": { 39 | "days": [ 40 | "Sat" 41 | ], 42 | "begin": "21:00:00+00:00", 43 | "end": "23:00:00+00:00" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/result/kyma/usecase/use_cases.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import "github.com/kyma-project/lifecycle-manager/internal/result" 4 | 5 | const ( 6 | SetKcpKymaStateDeleting result.UseCase = "SetKcpKymaStateDeleting" 7 | SetSkrKymaStateDeleting result.UseCase = "SetSkrKymaStateDeleting" 8 | DeleteSkrKyma result.UseCase = "DeleteSkrKyma" 9 | DeleteWatcherCertificateSetup result.UseCase = "DeleteCertificateSetup" 10 | DeleteSkrWebhookResources result.UseCase = "DeleteSkrWebhookResources" 11 | DeleteSkrModuleTemplateCrd result.UseCase = "DeleteSkrModuleTemplateCrd" 12 | DeleteSkrModuleReleaseMetaCrd result.UseCase = "DeleteSkrModuleReleaseMetaCrd" 13 | DeleteSkrKymaCrd result.UseCase = "DeleteSkrKymaCrd" 14 | DeleteManifests result.UseCase = "DeleteManifests" 15 | DeleteMetrics result.UseCase = "DeleteMetrics" 16 | DropKymaFinalizer result.UseCase = "DropKymaFinalizer" 17 | ) 18 | -------------------------------------------------------------------------------- /cmd/composition/repository/oci/ocirepository.go: -------------------------------------------------------------------------------- 1 | package oci 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/go-logr/logr" 7 | 8 | "github.com/kyma-project/lifecycle-manager/internal/manifest/spec" 9 | "github.com/kyma-project/lifecycle-manager/internal/repository/ocm" 10 | "github.com/kyma-project/lifecycle-manager/internal/repository/ocm/oci" 11 | ) 12 | 13 | func ComposeOCIRepository( 14 | kcl spec.KeyChainLookup, 15 | hostWithPort string, 16 | insecure bool, 17 | logger logr.Logger, 18 | bootstrapFailedExitCode int, 19 | ) *ocm.RepositoryReader { 20 | ociRepository, err := oci.NewRepository(kcl, insecure) 21 | if err != nil { 22 | logger.Error(err, "failed to create OCI repository") 23 | os.Exit(bootstrapFailedExitCode) 24 | } 25 | ocmRepository, err := ocm.NewRepository(hostWithPort, ociRepository) 26 | if err != nil { 27 | logger.Error(err, "failed to create OCI repository") 28 | os.Exit(bootstrapFailedExitCode) 29 | } 30 | return ocmRepository 31 | } 32 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Lifecycle Manager 2 | 3 | Kyma Lifecycle Manager (KLM) is a core component of the managed offering SAP BTP, Kyma runtime ("SKR"). Operating within the Kyma Control Plane (KCP) cluster, KLM manages the lifecycle of Kyma modules in SKR clusters. These SKR clusters are hyperscaler clusters provisioned for SKR users. 4 | 5 | KLM takes care of the following tasks: 6 | 7 | * Installing Custom Resource Definitions (CRDs) required for Kyma module deployment 8 | * Synchronizing the catalog of available Kyma modules to SKR clusters 9 | * Installing, updating, reconciling, and deleting Kyma module resources in SKR clusters 10 | * Watching SKR clusters for changes requested by the users 11 | 12 | KLM is built using the [Kubebuilder](https://github.com/kubernetes-sigs/kubebuilder) framework and extends the Kubernetes API through custom resource definitions. For detailed information about these resources, see [Lifecycle Manager Resources](./contributor/resources/README.md). 13 | -------------------------------------------------------------------------------- /.run/E2E Tests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | -------------------------------------------------------------------------------- /pkg/testutils/random/random.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "k8s.io/apimachinery/pkg/types" 7 | ) 8 | 9 | const ( 10 | randomNameLength = 8 11 | randomNameCharSet = "abcdefghijklmnopqrstuvwxyz" 12 | randomPortUpperBoundary = 65535 13 | ) 14 | 15 | // Name creates a random string [a-z] of len 8. 16 | func Name() string { 17 | b := make([]byte, randomNameLength) 18 | for i := range b { 19 | //nolint:gosec // random number generator sufficient for testing purposes 20 | b[i] = randomNameCharSet[rand.Intn(len(randomNameCharSet))] 21 | } 22 | return string(b) 23 | } 24 | 25 | func NamespacedName() types.NamespacedName { 26 | return types.NamespacedName{ 27 | Name: Name(), 28 | Namespace: Name(), 29 | } 30 | } 31 | 32 | // Port creates a random int64 in range [1, 65535]. 33 | func Port() int64 { 34 | //nolint:gosec // random number generator sufficient for testing purposes 35 | return int64(rand.Intn(randomPortUpperBoundary) + 1) 36 | } 37 | -------------------------------------------------------------------------------- /.run/Ensure Test Clusters.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | -------------------------------------------------------------------------------- /scripts/tests/add_skr_host_to_coredns.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | NEW_SKR_HOSTNAME="skr.cluster.local" 4 | HOST_IP_ADDRESS="" 5 | 6 | FOUND=0 7 | SECONDS=0 8 | TIMEOUT=60 9 | 10 | until [ $FOUND -eq 1 ]; do 11 | if [ $SECONDS -gt $TIMEOUT ]; then 12 | echo "Timeout reached. host.k3d.internal address not found." 13 | exit 1 14 | fi 15 | CURRENT_ENTRIES=$(kubectl get configmap coredns -n kube-system -o yaml | yq eval '.data.NodeHosts' -) 16 | if echo "$CURRENT_ENTRIES" | grep -q "host.k3d.internal"; then 17 | HOST_IP_ADDRESS=$(echo "$CURRENT_ENTRIES" | grep "host.k3d.internal" | awk '{print $1}') 18 | FOUND=1 19 | else 20 | sleep 5 21 | SECONDS=$((SECONDS+5)) 22 | fi 23 | done 24 | 25 | NEW_ENTRY="$HOST_IP_ADDRESS $NEW_SKR_HOSTNAME" 26 | 27 | kubectl get configmap coredns -n kube-system -o yaml | \ 28 | yq eval '.data.NodeHosts += "'"${NEW_ENTRY}"'"' - | \ 29 | kubectl apply -f - 30 | 31 | kubectl rollout restart -n kube-system deployment coredns -------------------------------------------------------------------------------- /scripts/tests/deploy_modulereleasemeta.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o nounset 3 | set -o errexit 4 | set -E 5 | set -o pipefail 6 | 7 | MODULE_NAME=$1 8 | shift 1 9 | CHANNELS=("$@") 10 | cat < module-release-meta.yaml 11 | apiVersion: operator.kyma-project.io/v1beta2 12 | kind: ModuleReleaseMeta 13 | metadata: 14 | name: ${MODULE_NAME} 15 | namespace: kcp-system 16 | spec: 17 | moduleName: ${MODULE_NAME} 18 | ocmComponentName: kyma-project.io/module/${MODULE_NAME} 19 | channels: 20 | EOF 21 | 22 | for CHANNEL in "${CHANNELS[@]}"; do 23 | IFS=':' read -r CHANNEL_NAME CHANNEL_VERSION <<< "${CHANNEL}" 24 | cat <> module-release-meta.yaml 25 | - channel: ${CHANNEL_NAME} 26 | version: ${CHANNEL_VERSION} 27 | EOF 28 | done 29 | kubectl apply -f module-release-meta.yaml 30 | 31 | echo "ModuleReleaseMeta created successfully" 32 | rm -f module-release-meta.yaml 33 | 34 | kubectl get modulereleasemeta "${MODULE_NAME}" -n kcp-system -o yaml 35 | kubectl get moduletemplate -n kcp-system 36 | -------------------------------------------------------------------------------- /scripts/tests/https_server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "flag" 6 | "log" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | const headerTimeout = 10 * time.Second 12 | 13 | func main() { 14 | directory := flag.String("dir", ".", "Directory to serve files from") 15 | certFile := flag.String("certfile", "cert.crt", "SSL certificate file") 16 | keyFile := flag.String("keyfile", "key.crt", "SSL key file") 17 | port := flag.String("port", "8080", "Port to run the server on") 18 | flag.Parse() 19 | 20 | mux := http.NewServeMux() 21 | fs := http.FileServer(http.Dir(*directory)) 22 | mux.Handle("/", fs) 23 | 24 | addr := ":" + *port 25 | httpsServer := &http.Server{ 26 | Addr: addr, 27 | Handler: mux, 28 | TLSConfig: &tls.Config{ 29 | MinVersion: tls.VersionTLS12, 30 | }, 31 | ReadHeaderTimeout: headerTimeout, 32 | } 33 | 34 | err := httpsServer.ListenAndServeTLS(*certFile, *keyFile) 35 | if err != nil { 36 | log.Fatal("failed to setup http server:", err) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.run/Deploy kyma.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | -------------------------------------------------------------------------------- /config/watcher/gateway.yaml: -------------------------------------------------------------------------------- 1 | # Use this Gateway for setup with enabled watcher if there is no istio gateway configured 2 | apiVersion: networking.istio.io/v1beta1 3 | kind: Gateway 4 | metadata: 5 | name: watcher 6 | labels: 7 | operator.kyma-project.io/watcher-gateway: default 8 | annotations: 9 | # When the client connection is mTLS, reset the XFCC header with the client certificate information and send it to the next hop. 10 | # This is being used to verify incoming requests 11 | "proxy.istio.io/config": '{"gatewayTopology" : { "forwardClientCertDetails": "SANITIZE_SET" } }' 12 | spec: 13 | selector: 14 | istio: ingressgateway # use istio default controller 15 | servers: 16 | - hosts: 17 | - 'listener.cp.kyma.cloud.sap' 18 | port: 19 | name: https 20 | number: 443 21 | protocol: HTTPS 22 | tls: 23 | credentialName: klm-istio-gateway 24 | mode: MUTUAL 25 | minProtocolVersion: TLSV1_3 26 | maxProtocolVersion: TLSV1_3 27 | -------------------------------------------------------------------------------- /pkg/templatelookup/moduletemplateinfolookup/common_test.go: -------------------------------------------------------------------------------- 1 | package moduletemplateinfolookup_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/kyma-project/lifecycle-manager/pkg/templatelookup/moduletemplateinfolookup" 9 | "github.com/kyma-project/lifecycle-manager/pkg/testutils/builder" 10 | ) 11 | 12 | func Test_TemplateNameMatch_WhenModuleNameFieldIsMatching(t *testing.T) { 13 | moduleTemplate := builder.NewModuleTemplateBuilder(). 14 | WithModuleName("test-module"). 15 | Build() 16 | 17 | isNameMatching := moduletemplateinfolookup.TemplateNameMatch(moduleTemplate, "test-module") 18 | assert.True(t, isNameMatching) 19 | } 20 | 21 | func Test_TemplateNameMatch_WhenModuleNameFieldIsNotMatching(t *testing.T) { 22 | moduleTemplate := builder.NewModuleTemplateBuilder(). 23 | WithModuleName("test-module"). 24 | Build() 25 | 26 | isNameMatching := moduletemplateinfolookup.TemplateNameMatch(moduleTemplate, "module") 27 | assert.False(t, isNameMatching) 28 | } 29 | -------------------------------------------------------------------------------- /scripts/tests/deploy_klm_from_registry.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$#" -ne 4 ]; then 4 | echo "Error: Exactly 2 arguments are required for both flags." 5 | echo "Usage: $0 --image-registry [dev../prod..] --image-tag latest" 6 | exit 1 7 | fi 8 | 9 | # Changing directory to the root of the project with git 10 | cd "$(git rev-parse --show-toplevel)" 11 | 12 | while [[ "$#" -gt 0 ]]; do 13 | case $1 in 14 | --image-registry) KLM_IMAGE_REGISTRY="$2"; shift ;; 15 | --image-tag) KLM_IMAGE_TAG="$2"; shift ;; 16 | *) 17 | echo "Unknown parameter passed: $1"; 18 | echo "Usage: $0 --image-registry [dev../prod..] --image-tag latest"; 19 | exit 1 ;; 20 | esac 21 | shift 22 | done 23 | 24 | export KUBECONFIG=${HOME}/.k3d/kcp-local.yaml 25 | IMG_REGISTRY_HOST="europe-docker.pkg.dev/kyma-project" 26 | IMG_NAME="lifecycle-manager" 27 | make local-deploy-with-watcher IMG=${IMG_REGISTRY_HOST}/${KLM_IMAGE_REGISTRY}/${IMG_NAME}:${KLM_IMAGE_TAG} 28 | 29 | echo "[$(basename $0)] KLM deployed successfully from registry" 30 | -------------------------------------------------------------------------------- /config/watcher/kyma_watcher.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operator.kyma-project.io/v1beta2 2 | kind: Watcher 3 | metadata: 4 | name: watcher 5 | labels: 6 | "operator.kyma-project.io/managed-by": "lifecycle-manager" 7 | spec: 8 | manager: "lifecycle-manager" 9 | labelsToWatch: 10 | "operator.kyma-project.io/watched-by": "kyma" 11 | resourceToWatch: 12 | group: operator.kyma-project.io 13 | version: "*" 14 | resource: kymas 15 | field: "spec" 16 | serviceInfo: 17 | name: klm-controller-manager-events 18 | port: 8082 19 | namespace: kcp-system 20 | gateway: 21 | selector: 22 | matchLabels: 23 | "operator.kyma-project.io/watcher-gateway": "default" 24 | --- 25 | apiVersion: v1 26 | kind: Service 27 | metadata: 28 | name: controller-manager-events 29 | spec: 30 | selector: 31 | app.kubernetes.io/name: lifecycle-manager 32 | ports: 33 | - protocol: TCP 34 | name: klm 35 | port: 8082 36 | targetPort: 8082 37 | - protocol: TCP 38 | name: kmm 39 | port: 8083 40 | targetPort: 8083 41 | -------------------------------------------------------------------------------- /docs/contributor/README.md: -------------------------------------------------------------------------------- 1 | # Lifecycle Manager Contributor Documentation 2 | 3 | * [Architecture](01-architecture.md) 4 | * [Lifecycle Manager Controllers](02-controllers.md) 5 | * [Provide Credentials for Private OCI Registry Authentication](03-config-private-registry.md) 6 | * [Configure a Local Test Setup (VS Code & GoLand)](04-local-test-setup.md) 7 | * [API Changelog](05-api-changelog.md) 8 | * [Lifecycle Manager Resources](resources/README.md) 9 | * [Kyma](resources/01-kyma.md) 10 | * [Manifest](resources/02-manifest.md) 11 | * [ModuleTemplate](resources/03-moduletemplate.md) 12 | * [Watcher](resources/04-watcher.md) 13 | * [ModuleReleaseMeta](resources/05-modulereleasemeta.md) 14 | * [Synchronization Between Kyma Control Plane and SAP BTP, Kyma Runtime](08-kcp-skr-synchronization.md) 15 | * [Lifecycle Manager Metrics](09-metrics.md) 16 | * [Maintenance Windows](10-maintenance-windows.md) 17 | * [Lifecycle Manager Components](11-components.md) 18 | * [Lifecycle Manager Flags](12-klm-arguments.md) 19 | * [Creating ModuleTemplate(using modulectl & ocm cli)](14-creating-moduletemplate.md) 20 | -------------------------------------------------------------------------------- /.run/Install Watcher Resources For Local KLM.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | -------------------------------------------------------------------------------- /internal/descriptor/types/ocmidentity/ocmidentity.go: -------------------------------------------------------------------------------- 1 | package ocmidentity 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | var ErrValueNotProvided = errors.New("value not provided") 9 | 10 | // ComponentId uniquely identifies an OCM ComponentId. 11 | // See: https://ocm.software/docs/overview/important-terms/#component-identity 12 | type ComponentId struct { 13 | componentName string 14 | componentVersion string 15 | } 16 | 17 | // NewComponentId is a constructor that ensures that both name and version are provided. 18 | func NewComponentId(name, version string) (*ComponentId, error) { 19 | if name == "" { 20 | return nil, fmt.Errorf("invalid component name: %w", ErrValueNotProvided) 21 | } 22 | if version == "" { 23 | return nil, fmt.Errorf("invalid component version: %w", ErrValueNotProvided) 24 | } 25 | 26 | return &ComponentId{ 27 | componentName: name, 28 | componentVersion: version, 29 | }, nil 30 | } 31 | 32 | func (c ComponentId) Name() string { 33 | return c.componentName 34 | } 35 | 36 | func (c ComponentId) Version() string { 37 | return c.componentVersion 38 | } 39 | -------------------------------------------------------------------------------- /internal/service/watcher/resources/skr_secret.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | apicorev1 "k8s.io/api/core/v1" 5 | apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | 7 | "github.com/kyma-project/lifecycle-manager/api/shared" 8 | "github.com/kyma-project/lifecycle-manager/internal/service/watcher/certificate/secret/data" 9 | ) 10 | 11 | const ( 12 | SkrTLSName = "skr-webhook-tls" 13 | ) 14 | 15 | func BuildSKRSecret(caCert, tlsCert, tlsKey []byte, remoteNs string) *apicorev1.Secret { 16 | return &apicorev1.Secret{ 17 | TypeMeta: apimetav1.TypeMeta{ 18 | Kind: "Secret", 19 | APIVersion: apicorev1.SchemeGroupVersion.String(), 20 | }, 21 | ObjectMeta: apimetav1.ObjectMeta{ 22 | Name: SkrTLSName, 23 | Namespace: remoteNs, 24 | Labels: map[string]string{ 25 | shared.ManagedBy: shared.ManagedByLabelValue, 26 | }, 27 | }, 28 | Immutable: nil, 29 | Data: map[string][]byte{ 30 | data.CaCertKey: caCert, 31 | data.TlsCertKey: tlsCert, 32 | data.TlsPrivateKeyKey: tlsKey, 33 | }, 34 | Type: apicorev1.SecretTypeOpaque, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/pkg/metrics/maintance_window.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" 6 | ) 7 | 8 | const ( 9 | MetricMaintenanceWindowConfigReadSuccess = "lifecycle_mgr_maintenance_window_config_read_success" 10 | ) 11 | 12 | type MaintenanceWindowMetrics struct { 13 | ConfigReadSuccessGauge prometheus.Gauge 14 | } 15 | 16 | func NewMaintenanceWindowMetrics() *MaintenanceWindowMetrics { 17 | metrics := &MaintenanceWindowMetrics{ 18 | ConfigReadSuccessGauge: prometheus.NewGauge(prometheus.GaugeOpts{ 19 | Name: MetricMaintenanceWindowConfigReadSuccess, 20 | Help: "Indicates whether the maintenance window configuration " + 21 | "was read successfully (1 for success, 0 for failure)", 22 | }), 23 | } 24 | ctrlmetrics.Registry.MustRegister(metrics.ConfigReadSuccessGauge) 25 | return metrics 26 | } 27 | 28 | func (m *MaintenanceWindowMetrics) RecordConfigReadSuccess(success bool) { 29 | value := 0.0 30 | if success { 31 | value = 1.0 32 | } 33 | m.ConfigReadSuccessGauge.Set(value) 34 | } 35 | -------------------------------------------------------------------------------- /.run/Deploy template-operator.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | -------------------------------------------------------------------------------- /internal/manifest/status/init.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "errors" 5 | 6 | "k8s.io/apimachinery/pkg/api/meta" 7 | apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | 9 | "github.com/kyma-project/lifecycle-manager/api/shared" 10 | "github.com/kyma-project/lifecycle-manager/api/v1beta2" 11 | ) 12 | 13 | var ErrObjectHasEmptyState = errors.New("object has an empty state") 14 | 15 | func Initialize(manifest *v1beta2.Manifest) error { 16 | status := manifest.GetStatus() 17 | 18 | for _, condition := range []apimetav1.Condition{ 19 | initResourcesCondition(manifest), 20 | initInstallationCondition(manifest), 21 | } { 22 | if meta.FindStatusCondition(status.Conditions, condition.Type) == nil { 23 | meta.SetStatusCondition(&status.Conditions, condition) 24 | } 25 | } 26 | 27 | if status.Synced == nil { 28 | status.Synced = []shared.Resource{} 29 | } 30 | 31 | if status.State == "" { 32 | manifest.SetStatus(status.WithState(shared.StateProcessing).WithErr(ErrObjectHasEmptyState)) 33 | return ErrObjectHasEmptyState 34 | } 35 | 36 | manifest.SetStatus(status) 37 | 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /internal/util/collections/filter.go: -------------------------------------------------------------------------------- 1 | package collections 2 | 3 | // FilterInPlace modifies a slice using provided predicate function. 4 | // It returns a sub-slice of the input list that only contains the elements 5 | // for which the predicate function returns true. 6 | // Warning: This function modifies the input list! 7 | func FilterInPlace[E any](list []*E, predicate func(*E) bool) []*E { 8 | last := 0 9 | for i := range list { 10 | if predicate(list[i]) { 11 | list[last] = list[i] 12 | last++ 13 | } 14 | } 15 | return list[:last] 16 | } 17 | 18 | // Filter returns a new slice which results from applying the provided predicate to the input slice. 19 | func Filter[E any](input []E, predicate func(E) bool) []E { 20 | output := []E{} 21 | for _, val := range input { 22 | if predicate(val) { 23 | output = append(output, val) 24 | } 25 | } 26 | return output 27 | } 28 | 29 | // Dereference is a function that dereferences elements of a provided slice of pointers. 30 | func Dereference[E any](list []*E) []E { 31 | res := make([]E, len(list)) 32 | for i := range list { 33 | res[i] = *list[i] 34 | } 35 | return res 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/report-sprint-commits.yml: -------------------------------------------------------------------------------- 1 | name: Report Sprint Test Suites 2 | run-name: A report for sprint commits 3 | 4 | permissions: { } 5 | 6 | # The report generation is performed ad hoc via manual invocation. 7 | on: workflow_dispatch 8 | 9 | env: 10 | reporter: ${{ github.workspace }}/scripts/coverage-metrics/bin/utils/commit-test-suites/gauge-sprint-commits.py 11 | pip_requirements: ${{ github.workspace }}/scripts/coverage-metrics/bin/utils/commit-test-suites/requirements.txt 12 | 13 | jobs: 14 | generate-report: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Check out report scripts 18 | uses: actions/checkout@v4 19 | with: 20 | repository: kyma-project/qa-toolkit 21 | path: scripts 22 | - name: Adjust the report scripts 23 | run: | 24 | chmod a+x $reporter 25 | python -m pip install --upgrade pip 26 | pip install -r $pip_requirements 27 | - name: Generate a report 28 | run: | 29 | $reporter --repo-url https://github.com/kyma-project/lifecycle-manager.git --days 14 --e2e tests/e2e --integration tests/integration 30 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | # This is a configuration file for the markdownlint. You can use this file to overwrite the default settings. 2 | # MD004 checks if the asterisk symbol is used for unordered list items 3 | MD004: 4 | style: asterisk 5 | # MD013 is set to false by default because many files include lines longer than the conventional 80 character limit 6 | MD013: false 7 | # MD024 disable the Multiple headings with the same content rule 8 | MD024: false 9 | # MD029 Set to false because it generates issues with longer lists 10 | MD029: false 11 | # MD033 is set to false to allow for inline HTML elements in a Markdown document 12 | MD033: false 13 | # MD044 is used to set capitalization for particular words. You can determine whether it should be used also for code blocks and HTML elements 14 | MD044: 15 | code_blocks: false 16 | html_elements: false 17 | names: 18 | - Kyma 19 | - Kubernetes 20 | - ConfigMap 21 | - CronJob 22 | - CustomResourceDefinition 23 | - Ingress 24 | - Node 25 | - PodPreset 26 | - Pod 27 | - ProwJob 28 | - Secret 29 | - ServiceBinding 30 | - ServiceClass 31 | - ServiceInstance 32 | -------------------------------------------------------------------------------- /tests/e2e/README.md: -------------------------------------------------------------------------------- 1 | # E2e Tests 2 | 3 | This subdirectory contains e2e tests used for the e2e verification of the watcher component. 4 | Those tests are written to run inside a pipeline. But they can also be run on a local machine. 5 | See the [Run the e2e tests locally](#run-the-e2e-tests-locally) section for details. 6 | 7 | ## Contents 8 | 9 | - [Test suite](./suite_test.go) that sets up required clients and configurations. 10 | - [Watcher end-to-end tests](./watcher_test.go) that includes tests and the end-to-end workflow of the watcher. 11 | - [Makefile](./Makefile) that includes targets to execute the watcher e2e test suite. 12 | 13 | ## Run the e2e tests locally 14 | 15 | 1. Set up your local environment using steps 1 to 3 from [this guide](/docs/contributor/04-local-test-setup.md). Create a cluster, and install Istio CRDs and `cert-manager`. Skip all the remaining steps from the guide. 16 | 2. Export the following K8s configurations as environment variables: 17 | ```shell 18 | export KCP_KUBECONFIG=$(k3d kubeconfig write kcp-local) 19 | export SKR_KUBECONFIG=$(k3d kubeconfig write skr-local) 20 | ``` 21 | 3. Run `make watcher-enqueue`. 22 | -------------------------------------------------------------------------------- /pkg/testutils/networkpolicy.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | apinetworkv1 "k8s.io/api/networking/v1" 8 | apierrors "k8s.io/apimachinery/pkg/api/errors" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | ) 11 | 12 | func NetworkPolicyExists(ctx context.Context, clnt client.Client, name, namespace string) error { 13 | networkPolicy, err := GetNetworkPolicy(ctx, clnt, name, namespace) 14 | return CRExists(networkPolicy, err) 15 | } 16 | 17 | func GetNetworkPolicy(ctx context.Context, clnt client.Client, name, namespace string) (*apinetworkv1.NetworkPolicy, 18 | error, 19 | ) { 20 | resource := &apinetworkv1.NetworkPolicy{} 21 | 22 | err := clnt.Get(ctx, client.ObjectKey{ 23 | Namespace: namespace, 24 | Name: name, 25 | }, resource) 26 | if err != nil { 27 | return nil, fmt.Errorf("get networkpolicy: %w", err) 28 | } 29 | return resource, nil 30 | } 31 | 32 | func CreateNetworkPolicy(ctx context.Context, clnt client.Client, networkPolicy *apinetworkv1.NetworkPolicy) error { 33 | if err := clnt.Create(ctx, networkPolicy); !apierrors.IsAlreadyExists(err) { 34 | return err 35 | } 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /.run/Deploy KLM from registry.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | -------------------------------------------------------------------------------- /internal/istio/errors.go: -------------------------------------------------------------------------------- 1 | package istio 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrFailedToCreateIstioClient = errors.New("failed to create istio client from config") 7 | ErrFailedToGetVirtualService = errors.New("failed to get virtual service") 8 | ErrFailedToListVirtualServices = errors.New("failed to list virtual services") 9 | ErrFailedToCreateVirtualService = errors.New("failed to create virtual service") 10 | ErrFailedToUpdateVirtualService = errors.New("failed to update virtual service") 11 | ErrFailedToDeleteVirtualService = errors.New("failed to delete virtual service") 12 | ErrFailedToConvertLabelSelector = errors.New("failed to convert label selector to selector") 13 | ErrFailedToGetGatewayByLabelSelector = errors.New("failed to get gateway by label selector") 14 | ErrFailedToAddOwnerReference = errors.New("failed to add owner reference") 15 | ErrCantFindMatchingGateway = errors.New("can't find matching Istio Gateway") 16 | ErrInvalidArgument = errors.New("invalid argument") 17 | ErrCantFindGatewayServersHost = errors.New("can't find Istio Gateway servers hosts") 18 | ) 19 | -------------------------------------------------------------------------------- /.run/Create New Test Clusters.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 20 | -------------------------------------------------------------------------------- /internal/pkg/metrics/godebug_internal_test.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestParseGodebugFips140only(t *testing.T) { 8 | t.Parallel() 9 | 10 | tests := []struct { 11 | godebug string 12 | want string 13 | }{ 14 | {"fips140=only", "only"}, 15 | {"fips140=on", "on"}, 16 | {"fips140=debug", "debug"}, 17 | {"fips140=off", "off"}, 18 | { 19 | "fips140=someOtherValue", 20 | "off", 21 | }, // Such a value is refused by standard library at runtime, but here let's just default to "off" 22 | {"fips140=debug, fips140=only", "only"}, 23 | {"fips140=off,fips140=on,fips140=only", "only"}, // The last one wins 24 | {"fips140=only, fips140=on, fips140=off", "off"}, // Spaces around values should not affect the result 25 | {"fips140=only , fips140=off ,fips140=on", "on"}, // Spaces around values should not affect the result 26 | } 27 | 28 | for _, tt := range tests { 29 | t.Run(tt.godebug, func(t *testing.T) { 30 | t.Parallel() 31 | if got := parseGodebugFipsMode(tt.godebug); got != tt.want { 32 | t.Errorf("parseGodebugFips140only(%q) = %v, want %v", tt.godebug, got, tt.want) 33 | } 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/declarative/v2/resource_test.go: -------------------------------------------------------------------------------- 1 | package v2_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "k8s.io/apimachinery/pkg/api/meta" 8 | "k8s.io/apimachinery/pkg/runtime/schema" 9 | "k8s.io/cli-runtime/pkg/resource" 10 | 11 | declarativev2 "github.com/kyma-project/lifecycle-manager/internal/declarative/v2" 12 | ) 13 | 14 | func newInfo(name, namespace, kind string) *resource.Info { 15 | return &resource.Info{ 16 | Name: name, 17 | Namespace: namespace, 18 | Mapping: &meta.RESTMapping{ 19 | GroupVersionKind: schema.GroupVersionKind{Kind: kind}, 20 | }, 21 | } 22 | } 23 | 24 | func TestResourceList_Difference(t *testing.T) { 25 | dummyPod := newInfo("foo", "default", "Pod") 26 | dummyService := newInfo("bar", "default", "Service") 27 | dummyDeploy := newInfo("baz", "default", "Deployment") 28 | 29 | list1 := declarativev2.ResourceList{dummyPod, dummyService, dummyDeploy} 30 | list2 := declarativev2.ResourceList{dummyService} 31 | 32 | diff := list1.Difference(list2) 33 | 34 | assert.Len(t, diff, 2) 35 | assert.Contains(t, diff, dummyPod) 36 | assert.Contains(t, diff, dummyDeploy) 37 | assert.NotContains(t, diff, dummyService) 38 | } 39 | -------------------------------------------------------------------------------- /.github/scripts/release/create_changelog.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | set -o errexit 5 | set -E 6 | set -o pipefail 7 | 8 | CURRENT_RELEASE_TAG=$1 9 | DOCKER_IMAGE_URL=$2 10 | LAST_RELEASE_TAG=$3 11 | 12 | if [ "${LAST_RELEASE_TAG}" == "" ] 13 | then 14 | LAST_RELEASE_TAG=$(git describe --tags --abbrev=0) 15 | fi 16 | 17 | GITHUB_API_URL=https://api.github.com/repos/$CODE_REPOSITORY 18 | GITHUB_AUTH_HEADER="Authorization: Bearer $GITHUB_TOKEN" 19 | GITHUB_CHANGELOG_URL=https://github.com/$CODE_REPOSITORY 20 | CHANGELOG_FILE="CHANGELOG.md" 21 | 22 | git log "$LAST_RELEASE_TAG"..HEAD --pretty=tformat:"%h" --reverse | while read -r commit 23 | do 24 | COMMIT_AUTHOR=$(curl -H "$GITHUB_AUTH_HEADER" -sS "$GITHUB_API_URL"/commits/"$commit" | jq -r '.author.login') 25 | if [ "${COMMIT_AUTHOR}" != "kyma-bot" ]; then 26 | git show -s "${commit}" --format="* %s by @${COMMIT_AUTHOR}" >> ${CHANGELOG_FILE} 27 | fi 28 | done 29 | 30 | { 31 | echo -e "\n**Full changelog**: $GITHUB_CHANGELOG_URL/compare/$LAST_RELEASE_TAG...$CURRENT_RELEASE_TAG" 32 | echo -e "\n" 33 | echo "## Docker image URL" 34 | echo "$DOCKER_IMAGE_URL" 35 | } >> $CHANGELOG_FILE 36 | 37 | cat $CHANGELOG_FILE 38 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "github-actions" 6 | directories: 7 | - "/" 8 | - "/.github/actions" 9 | schedule: 10 | interval: "weekly" 11 | commit-message: 12 | prefix: "chore(dependabot)" 13 | - package-ecosystem: "gomod" 14 | directories: 15 | - "/" 16 | - "/api" 17 | - "/maintenancewindows" 18 | labels: 19 | - "go" 20 | - "area/dependency" 21 | schedule: 22 | interval: "daily" 23 | commit-message: 24 | prefix: "chore(dependabot)" 25 | groups: 26 | k8s-dependencies: 27 | patterns: 28 | - "k8s.io/*" 29 | istio-dependencies: 30 | patterns: 31 | - "istio.io/*" 32 | ginkgo-gomega: 33 | patterns: 34 | - "github.com/onsi/ginkgo/v2" 35 | - "github.com/onsi/gomega" 36 | - package-ecosystem: "docker" 37 | directory: "/" 38 | labels: 39 | - "docker" 40 | - "area/dependency" 41 | schedule: 42 | interval: "daily" 43 | commit-message: 44 | prefix: "chore(dependabot)" 45 | -------------------------------------------------------------------------------- /config/gardener-certmanager/certificate_webhook.yaml: -------------------------------------------------------------------------------- 1 | # The following manifests contain a self-signed issuer CR and a certificate CR. 2 | # More document can be found at https://docs.cert-manager.io 3 | # WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes. 4 | apiVersion: cert.gardener.cloud/v1alpha1 5 | kind: Issuer 6 | metadata: 7 | name: controller-manager-selfsigned 8 | namespace: default 9 | spec: 10 | selfSigned: { } 11 | --- 12 | apiVersion: cert.gardener.cloud/v1alpha1 13 | kind: Certificate 14 | metadata: 15 | name: controller-manager-webhook-serving # this name should match the one appeared in kustomizeconfig.yaml 16 | namespace: kcp-system 17 | spec: 18 | commonName: klm-controller-manager-webhook-serving 19 | isCA: true 20 | # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize 21 | dnsNames: 22 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc 23 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local 24 | issuerRef: 25 | name: controller-manager-selfsigned 26 | namespace: default 27 | secretName: klm-controller-manager-webhook # this secret will not be prefixed, since it's not managed by kustomize 28 | -------------------------------------------------------------------------------- /internal/service/kyma/deletion/usecases/delete_metrics.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyma-project/lifecycle-manager/api/v1beta2" 7 | "github.com/kyma-project/lifecycle-manager/internal/result" 8 | "github.com/kyma-project/lifecycle-manager/internal/result/kyma/usecase" 9 | ) 10 | 11 | type KymaMetrics interface { 12 | HasMetrics(kymaName string) (bool, error) 13 | CleanupMetrics(kymaName string) 14 | } 15 | 16 | type DeleteMetrics struct { 17 | kymaMetrics KymaMetrics 18 | } 19 | 20 | func NewDeleteMetrics(kymaMetrics KymaMetrics) *DeleteMetrics { 21 | return &DeleteMetrics{ 22 | kymaMetrics: kymaMetrics, 23 | } 24 | } 25 | 26 | func (u *DeleteMetrics) IsApplicable(_ context.Context, kyma *v1beta2.Kyma) (bool, error) { 27 | hasMetrics, err := u.kymaMetrics.HasMetrics(kyma.GetName()) 28 | if err != nil { 29 | return false, err 30 | } 31 | 32 | return hasMetrics, nil 33 | } 34 | 35 | func (u *DeleteMetrics) Execute(_ context.Context, kyma *v1beta2.Kyma) result.Result { 36 | u.kymaMetrics.CleanupMetrics(kyma.GetName()) 37 | return result.Result{UseCase: u.Name()} 38 | } 39 | 40 | func (u *DeleteMetrics) Name() result.UseCase { 41 | return usecase.DeleteMetrics 42 | } 43 | -------------------------------------------------------------------------------- /.github/actions/setup-test-clusters-gcm/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup test clusters 2 | description: Creates and configures the KCP and SKR clusters. 3 | inputs: 4 | k8s_version: 5 | description: The version of k8s to use. For example, 1.28.7 6 | required: true 7 | gardener_cert_manager_version: 8 | description: The version of gardener cert-manager to deploy. For example, 0.17.5. 9 | required: true 10 | gardener_cert_manager_renewal_window: 11 | description: The duration before the certificate expiration when the renewal should be triggered. Default is 720h. 12 | required: true 13 | runs: 14 | using: composite 15 | steps: 16 | - name: create-test-clusters 17 | shell: bash 18 | run: | 19 | ./lifecycle-manager/scripts/tests/create_test_clusters.sh --k8s-version ${{ inputs.k8s_version }} \ 20 | --gardener-cert-manager-version ${{ inputs.gardener_cert_manager_version }} \ 21 | --gardener-cert-manager-renewal-window ${{ inputs.gardener_cert_manager_renewal_window }} 22 | 23 | - uses: ./lifecycle-manager/.github/actions/export-kubeconfigs 24 | 25 | - uses: ./lifecycle-manager/.github/actions/switch-kubectl-context 26 | with: 27 | context_name: k3d-kcp 28 | -------------------------------------------------------------------------------- /internal/declarative/v2/state_check.go: -------------------------------------------------------------------------------- 1 | package v2 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "k8s.io/cli-runtime/pkg/resource" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | 11 | "github.com/kyma-project/lifecycle-manager/api/shared" 12 | ) 13 | 14 | var ErrNotValidClientObject = errors.New("object in resource info is not a valid client object") 15 | 16 | type StateCheck interface { 17 | GetState(ctx context.Context, clnt client.Client, resources []*resource.Info) (shared.State, error) 18 | } 19 | 20 | type ExistsStateCheck struct{} 21 | 22 | func NewExistsStateCheck() *ExistsStateCheck { 23 | return &ExistsStateCheck{} 24 | } 25 | 26 | func (c *ExistsStateCheck) GetState( 27 | ctx context.Context, 28 | clnt client.Client, 29 | resources []*resource.Info, 30 | ) (shared.State, error) { 31 | for i := range resources { 32 | obj, ok := resources[i].Object.(client.Object) 33 | if !ok { 34 | return shared.StateError, ErrNotValidClientObject 35 | } 36 | if err := clnt.Get(ctx, client.ObjectKeyFromObject(obj), obj); client.IgnoreNotFound(err) != nil { 37 | return shared.StateError, fmt.Errorf("failed to fetch object by key: %w", err) 38 | } 39 | } 40 | return shared.StateReady, nil 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/report-acceptance-criteria.yml: -------------------------------------------------------------------------------- 1 | name: Report Acceptance Criteria 2 | run-name: Generate AC coverage report 3 | 4 | permissions: { } 5 | 6 | # The report generation is performed ad hoc via manual invocation. 7 | on: workflow_dispatch 8 | 9 | env: 10 | glossaryURL: https://github.com/kyma-project/lifecycle-manager/wiki/Glossary.md 11 | # The report highlighter utility that is checked out from the 12 | # https://github.com/kyma-project/qa-toolkit/blob/main/coverage-metrics/bin/utils/report-highlighter/highlighter.py 13 | highlighter: ${{ github.workspace }}/scripts/coverage-metrics/bin/utils/report-highlighter/highlighter.py 14 | 15 | jobs: 16 | generate-report: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Check out LM source code 20 | uses: actions/checkout@v4 21 | - name: Check out report highlighter 22 | uses: actions/checkout@v4 23 | with: 24 | repository: kyma-project/qa-toolkit 25 | path: scripts 26 | - name: Prepare the highlighter utility 27 | run: | 28 | chmod a+x $highlighter 29 | - name: Genarate a report 30 | run: cd ${{ github.workspace }}/tests/e2e && make e2e-coverage | $highlighter $glossaryURL 31 | -------------------------------------------------------------------------------- /pkg/templatelookup/moduletemplateinfolookup/common.go: -------------------------------------------------------------------------------- 1 | package moduletemplateinfolookup 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | 9 | "github.com/kyma-project/lifecycle-manager/api/v1beta2" 10 | ) 11 | 12 | func TemplateNameMatch(template *v1beta2.ModuleTemplate, name string) bool { 13 | return template.Spec.ModuleName == name 14 | } 15 | 16 | func getTemplateByVersion(ctx context.Context, 17 | clnt client.Reader, 18 | moduleName, 19 | moduleVersion, 20 | namespace string, 21 | ) (*v1beta2.ModuleTemplate, error) { 22 | moduleTemplate := &v1beta2.ModuleTemplate{} 23 | 24 | moduleTemplateName := v1beta2.CreateModuleTemplateName(moduleName, moduleVersion) 25 | if err := clnt.Get(ctx, client.ObjectKey{ 26 | Name: moduleTemplateName, 27 | Namespace: namespace, 28 | }, moduleTemplate); err != nil { 29 | return nil, fmt.Errorf("failed to get module template: %w", err) 30 | } 31 | 32 | return moduleTemplate, nil 33 | } 34 | 35 | func getDesiredChannel(moduleChannel, globalChannel string) string { 36 | if moduleChannel != "" { 37 | return moduleChannel 38 | } 39 | if globalChannel != "" { 40 | return globalChannel 41 | } 42 | return v1beta2.DefaultChannel 43 | } 44 | -------------------------------------------------------------------------------- /docs/contributor/adr/005-consistent-naming.md: -------------------------------------------------------------------------------- 1 | # ADR 005 - Consistent Naming 2 | 3 | ## Status 4 | 5 | Accepted 6 | 7 | ## Context 8 | 9 | We must decide how to consistently name things in the project. 10 | 11 | ## Decision 12 | 13 | It is decided that the following naming patterns apply: 14 | 15 | ### Layered Architecture 16 | 17 | Major building blocks, namely types, of the [layered architecture](004-layered-architecture.md) are *controllers*, *services*, and *repositories*. It is decided that: 18 | - The types are suffixed accordingly. 19 | - We **DON'T** use *Interface* and *Impl* suffixes. 20 | - The implementation types are not prefixed with the context. The context is already established by the package name. 21 | 22 | #### Do's 23 | 24 | ```go 25 | // package internal/controller/foo 26 | type Controller struct { } // => foo.Controller 27 | type BarService interface { } 28 | 29 | // package internal/service/bar 30 | type Service struct { } // => bar.Service 31 | type BazRepository interface { } 32 | 33 | // package internal/repository/baz 34 | type Repository struct { } // => baz.Repository 35 | ``` 36 | 37 | ## Consequences 38 | 39 | We apply consistent naming within the project. 40 | This ADR may be extended with further naming guidelines. 41 | -------------------------------------------------------------------------------- /docs/contributor/adr/000-document-decisions-as-adrs.md: -------------------------------------------------------------------------------- 1 | # ADR 000 - Decisions are documented as ADRs in `/docs/contributor/adr` 2 | 3 | ## Status 4 | 5 | Accepted 6 | 7 | ## Context 8 | 9 | Continuously working on lifecycle-manager continuously requires decisions to be taken concerning lifecycle-manager. 10 | 11 | It needs to be decided where and how such decisions are documented. 12 | 13 | ## Decision 14 | 15 | It is decided that outside-facing decisions that are aligned with other Kyma teams are continued to be documented as Issues in the `kyma-project/community` repository. This is the general approach other teams follow as well. 16 | 17 | For decisions internal to lifecycle-manager, for example, implementation- or architecture-related decisions, the following applies: 18 | 19 | - decisions MUST follow the [ADR template by Michael Nygard](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions) 20 | - decisions MUST be stored as Markdown files in `/docs/contributor/adr` 21 | - decisions MUST be aligned within the @kyma-project/jellyfish team 22 | - decisions MUST be contributed as a PR 23 | 24 | ## Consequences 25 | 26 | Each new significant decision about internal lifecycle-manager implementation or architecture is documented as an ADR. 27 | -------------------------------------------------------------------------------- /internal/service/kyma/deletion/usecases/delete_manifests.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyma-project/lifecycle-manager/api/v1beta2" 7 | "github.com/kyma-project/lifecycle-manager/internal/result" 8 | "github.com/kyma-project/lifecycle-manager/internal/result/kyma/usecase" 9 | ) 10 | 11 | type ManifestRepo interface { 12 | ExistForKyma(ctx context.Context, kymaName string) (bool, error) 13 | DeleteAllForKyma(ctx context.Context, kymaName string) error 14 | } 15 | 16 | type DeleteManifests struct { 17 | manifestRepo ManifestRepo 18 | } 19 | 20 | func NewDeleteManifests(manifestRepo ManifestRepo) *DeleteManifests { 21 | return &DeleteManifests{ 22 | manifestRepo: manifestRepo, 23 | } 24 | } 25 | 26 | func (u *DeleteManifests) IsApplicable(ctx context.Context, kcpKyma *v1beta2.Kyma) (bool, error) { 27 | return u.manifestRepo.ExistForKyma(ctx, kcpKyma.GetName()) 28 | } 29 | 30 | func (u *DeleteManifests) Execute(ctx context.Context, kcpKyma *v1beta2.Kyma) result.Result { 31 | return result.Result{ 32 | UseCase: u.Name(), 33 | Err: u.manifestRepo.DeleteAllForKyma(ctx, kcpKyma.GetName()), 34 | } 35 | } 36 | 37 | func (u *DeleteManifests) Name() result.UseCase { 38 | return usecase.DeleteManifests 39 | } 40 | -------------------------------------------------------------------------------- /internal/pkg/metrics/watcher.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | watchermetrics "github.com/kyma-project/runtime-watcher/listener/pkg/metrics" 5 | "github.com/prometheus/client_golang/prometheus" 6 | ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" 7 | ) 8 | 9 | const ( 10 | MetricSelfSignedCertNotRenew = "lifecycle_mgr_self_signed_cert_not_renew" 11 | ) 12 | 13 | type WatcherMetrics struct { 14 | certNotRenewGauge *prometheus.GaugeVec 15 | } 16 | 17 | func NewWatcherMetrics() *WatcherMetrics { 18 | watcherMetrics := &WatcherMetrics{ 19 | certNotRenewGauge: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 20 | Name: MetricSelfSignedCertNotRenew, 21 | Help: "Indicates the self-signed Certificate of related Kyma is not renewed yet", 22 | }, []string{KymaNameLabel}), 23 | } 24 | ctrlmetrics.Registry.MustRegister(watcherMetrics.certNotRenewGauge) 25 | watchermetrics.Init(ctrlmetrics.Registry) 26 | return watcherMetrics 27 | } 28 | 29 | func (w *WatcherMetrics) CleanupMetrics(kymaName string) { 30 | w.certNotRenewGauge.DeletePartialMatch(prometheus.Labels{ 31 | KymaNameLabel: kymaName, 32 | }) 33 | } 34 | 35 | func (w *WatcherMetrics) SetCertNotRenew(kymaName string) { 36 | w.certNotRenewGauge.With(prometheus.Labels{ 37 | KymaNameLabel: kymaName, 38 | }).Set(1) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/log/logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "github.com/go-logr/logr" 5 | "github.com/go-logr/zapr" 6 | "go.uber.org/zap" 7 | "go.uber.org/zap/zapcore" 8 | k8szap "sigs.k8s.io/controller-runtime/pkg/log/zap" //nolint:importas // a one-time reference for the package 9 | ) 10 | 11 | func ConfigLogger(level int8, syncer zapcore.WriteSyncer) logr.Logger { 12 | if level > 0 { 13 | level = -level 14 | } 15 | // The following settings is based on kyma community Improvement of log messages usability 16 | //nolint:revive // https://github.com/kyma-project/community/blob/main/concepts/observability-consistent-logging/improvement-of-log-messages-usability.md#log-structure 17 | atomicLevel := zap.NewAtomicLevelAt(zapcore.Level(level)) 18 | encoderConfig := zap.NewProductionEncoderConfig() 19 | encoderConfig.TimeKey = "date" 20 | encoderConfig.EncodeTime = zapcore.RFC3339NanoTimeEncoder 21 | encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder 22 | encoder := zapcore.NewJSONEncoder(encoderConfig) 23 | 24 | core := zapcore.NewCore( 25 | &k8szap.KubeAwareEncoder{Encoder: encoder}, syncer, atomicLevel, 26 | ) 27 | zapLog := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel)) 28 | logger := zapr.NewLogger(zapLog.With(zap.Namespace("context"))) 29 | return logger 30 | } 31 | -------------------------------------------------------------------------------- /.run/Launch KLM locally.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /internal/service/watcher/certificate/secret/data/data.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | apicorev1 "k8s.io/api/core/v1" 8 | ) 9 | 10 | const ( 11 | CaCertKey = "ca.crt" 12 | TlsCertKey = "tls.crt" 13 | TlsPrivateKeyKey = "tls.key" 14 | ) 15 | 16 | var ErrSecretDataMissing = errors.New("secret data is missing") 17 | 18 | type CertificateSecretData struct { 19 | TlsCert, TlsKey []byte 20 | } 21 | 22 | type GatewaySecretData struct { 23 | CaCert []byte 24 | } 25 | 26 | func NewGatewaySecretData(secret *apicorev1.Secret) (*GatewaySecretData, error) { 27 | if secret == nil || secret.Data == nil || secret.Data[CaCertKey] == nil { 28 | return nil, fmt.Errorf("error in gateway secret %w", ErrSecretDataMissing) 29 | } 30 | return &GatewaySecretData{ 31 | CaCert: secret.Data[CaCertKey], 32 | }, nil 33 | } 34 | 35 | func NewCertificateSecretData(secret *apicorev1.Secret) (*CertificateSecretData, error) { 36 | if secret == nil || secret.Data == nil || secret.Data[TlsCertKey] == nil || secret.Data[TlsPrivateKeyKey] == nil { 37 | return nil, fmt.Errorf("error in certificate secret %w", ErrSecretDataMissing) 38 | } 39 | 40 | return &CertificateSecretData{ 41 | TlsCert: secret.Data[TlsCertKey], 42 | TlsKey: secret.Data[TlsPrivateKeyKey], 43 | }, nil 44 | } 45 | -------------------------------------------------------------------------------- /api/v1beta2/manifest_types_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1beta2_test 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/require" 23 | 24 | "github.com/kyma-project/lifecycle-manager/api/shared" 25 | "github.com/kyma-project/lifecycle-manager/api/v1beta2" 26 | ) 27 | 28 | func TestGetCacheKey(t *testing.T) { 29 | manifest := &v1beta2.Manifest{} 30 | manifest.SetName("test-manifest") 31 | manifest.SetNamespace("test-namespace") 32 | manifest.SetLabels(map[string]string{ 33 | shared.KymaName: "kyma-test", 34 | }) 35 | 36 | expectedKey := "kyma-test|test-namespace" 37 | 38 | key, found := manifest.GenerateCacheKey() 39 | require.True(t, found) 40 | require.Equal(t, expectedKey, key) 41 | } 42 | -------------------------------------------------------------------------------- /internal/descriptor/types/descriptor.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | machineryruntime "k8s.io/apimachinery/pkg/runtime" 8 | "k8s.io/apimachinery/pkg/runtime/schema" 9 | "ocm.software/ocm/api/ocm/compdesc" 10 | ) 11 | 12 | var ErrDecode = errors.New("failed to decode component descriptor") 13 | 14 | type Descriptor struct { 15 | *compdesc.ComponentDescriptor 16 | } 17 | 18 | func (d *Descriptor) SetGroupVersionKind(kind schema.GroupVersionKind) { 19 | d.Version = kind.Version 20 | } 21 | 22 | func (d *Descriptor) GroupVersionKind() schema.GroupVersionKind { 23 | return schema.GroupVersionKind{ 24 | Group: "ocm.kyma-project.io", 25 | Version: d.Metadata.ConfiguredVersion, 26 | Kind: "Descriptor", 27 | } 28 | } 29 | 30 | func (d *Descriptor) GetObjectKind() schema.ObjectKind { 31 | return d 32 | } 33 | 34 | func (d *Descriptor) DeepCopyObject() machineryruntime.Object { 35 | return &Descriptor{ComponentDescriptor: d.Copy()} 36 | } 37 | 38 | // Deserialize decodes the component descriptor from its serialized form. 39 | func Deserialize(compdescBytes []byte) (*compdesc.ComponentDescriptor, error) { 40 | desc, err := compdesc.Decode(compdescBytes) 41 | if err != nil { 42 | return nil, fmt.Errorf("%w: %w", ErrDecode, err) 43 | } 44 | return desc, nil 45 | } 46 | -------------------------------------------------------------------------------- /pkg/templatelookup/moduletemplateinfo_test.go: -------------------------------------------------------------------------------- 1 | package templatelookup_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/kyma-project/lifecycle-manager/api/v1beta2" 10 | "github.com/kyma-project/lifecycle-manager/pkg/templatelookup" 11 | "github.com/kyma-project/lifecycle-manager/pkg/testutils" 12 | ) 13 | 14 | func Test_GetOCMIdentity(t *testing.T) { 15 | t.Run("When ComponentIdentity is nil, then an error is returned", func(t *testing.T) { 16 | mtInfo := templatelookup.ModuleTemplateInfo{ 17 | ModuleTemplate: &v1beta2.ModuleTemplate{}, 18 | ComponentId: nil, 19 | } 20 | _, err := mtInfo.GetOCMIdentity() 21 | require.Error(t, err) 22 | require.ErrorIs(t, err, templatelookup.ErrNoIdentity) 23 | assert.Contains(t, err.Error(), "component identity is nil") 24 | }) 25 | t.Run("When ComponentIdentity is not nil, then it is returned", func(t *testing.T) { 26 | mtInfo := templatelookup.ModuleTemplateInfo{ 27 | ComponentId: testutils.MustNewComponentId(testutils.FullOCMName("test-module"), "1.0.0"), 28 | } 29 | comp, err := mtInfo.GetOCMIdentity() 30 | require.NoError(t, err) 31 | assert.Equal(t, "kyma-project.io/module/test-module", comp.Name()) 32 | assert.Equal(t, "1.0.0", comp.Version()) 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /tests/integration/controller/kyma/kyma_module_version_test.go: -------------------------------------------------------------------------------- 1 | package kyma_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | 7 | "github.com/kyma-project/lifecycle-manager/api/v1beta2" 8 | 9 | . "github.com/kyma-project/lifecycle-manager/pkg/testutils" 10 | ) 11 | 12 | var _ = Describe("Given invalid module version which is rejected by CRD validation rules", func() { 13 | DescribeTable( 14 | "Test enable module", func(givenCondition func() error) { 15 | Skip("Version attribute is disabled for now on the CRD level") 16 | Eventually(givenCondition, Timeout, Interval).Should(Succeed()) 17 | }, 18 | 19 | Entry( 20 | "invalid semantic version", 21 | givenKymaWithInvalidModuleVersion("20240101"), 22 | ), 23 | Entry( 24 | "invalid semantic version", 25 | givenKymaWithInvalidModuleVersion("1.0.0.abc"), 26 | ), 27 | ) 28 | }) 29 | 30 | func givenKymaWithInvalidModuleVersion(version string) func() error { 31 | return func() error { 32 | kyma := NewTestKyma("kyma") 33 | kyma.Spec.Channel = v1beta2.DefaultChannel 34 | module := NewTestModuleWithChannelVersion("test", v1beta2.DefaultChannel, version) 35 | kyma.Spec.Modules = append( 36 | kyma.Spec.Modules, module) 37 | err := kcpClient.Create(ctx, kyma) 38 | return ignoreInvalidError(err) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /internal/tests/api/label_annotations_test.go: -------------------------------------------------------------------------------- 1 | package api_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/kyma-project/lifecycle-manager/api/shared" 9 | ) 10 | 11 | func Test_AnnotationHasExternalDependencies(t *testing.T) { 12 | t.Parallel() 13 | assert.Equal(t, "operator.kyma-project.io/is-cluster-scoped", shared.IsClusterScopedAnnotation) 14 | assert.Equal(t, "skr-domain", shared.SkrDomainAnnotation) 15 | } 16 | 17 | func Test_LabelHasExternalDependencies(t *testing.T) { 18 | t.Parallel() 19 | assert.Equal(t, "kyma-project.io/instance-id", shared.InstanceIDLabel) 20 | assert.Equal(t, "operator.kyma-project.io/skip-reconciliation", shared.SkipReconcileLabel) 21 | assert.Equal(t, "operator.kyma-project.io/internal", shared.InternalLabel) 22 | assert.Equal(t, "operator.kyma-project.io/beta", shared.BetaLabel) 23 | assert.Equal(t, "operator.kyma-project.io/module-name", shared.ModuleName) 24 | assert.Equal(t, "operator.kyma-project.io/kyma-name", shared.KymaName) 25 | assert.Equal(t, "operator.kyma-project.io/channel", shared.ChannelLabel) 26 | assert.Equal(t, "operator.kyma-project.io/managed-by", shared.ManagedBy) 27 | assert.Equal(t, "istio-injection", shared.IstioInjectionLabel) 28 | assert.Equal(t, "namespaces.warden.kyma-project.io/validate", shared.WardenLabel) 29 | } 30 | -------------------------------------------------------------------------------- /.github/actions/wait-for-image-build/action.yml: -------------------------------------------------------------------------------- 1 | name: Wait for image build 2 | description: Waits for the image to be built. Exits with error if not built within the given timeout. 3 | inputs: 4 | token: 5 | description: The GitHub token to use for making API requests. 6 | required: true 7 | statusName: 8 | description: The name of the GitHub status check to wait for. For example, `build` or `deploy`. 9 | required: true 10 | timeoutSeconds: 11 | description: The number of seconds to wait for the status check to complete. 12 | required: false 13 | default: "1200" 14 | intervalSeconds: 15 | description: The number of seconds to wait before each poll of the GitHub API. 16 | required: false 17 | default: "10" 18 | runs: 19 | using: composite 20 | steps: 21 | - name: Wait for image build 22 | id: wait-for-build 23 | with: 24 | token: ${{ inputs.token }} 25 | statusName: ${{ inputs.statusName }} 26 | timeoutSeconds: ${{ inputs.timeoutSeconds }} 27 | intervalSeconds: ${{ inputs.intervalSeconds }} 28 | uses: autotelic/action-wait-for-status-check@v1 29 | - name: Exit if build failed 30 | if: steps.wait-for-build.outputs.state != 'success' 31 | shell: bash 32 | run: | 33 | echo "Image build did not succeed!" 34 | exit 1 35 | -------------------------------------------------------------------------------- /internal/service/mandatorymodule/deletion/usecases/remove_finalizer.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | 6 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 7 | 8 | "github.com/kyma-project/lifecycle-manager/api/shared" 9 | "github.com/kyma-project/lifecycle-manager/api/v1beta2" 10 | ) 11 | 12 | type MrmRemoFinalizerRepo interface { 13 | RemoveFinalizer(ctx context.Context, moduleName string, finalizer string) error 14 | } 15 | 16 | // RemoveFinalizer is responsible for removing the mandatory finalizer from the ModuleReleaseMeta. 17 | type RemoveFinalizer struct { 18 | repo MrmRemoFinalizerRepo 19 | } 20 | 21 | func NewRemoveFinalizer(repo MrmRemoFinalizerRepo) *RemoveFinalizer { 22 | return &RemoveFinalizer{repo: repo} 23 | } 24 | 25 | // IsApplicable returns true if the ModuleReleaseMeta contains the mandatory finalizer, so it should be removed. 26 | // This should be the last step in the deletion process. 27 | func (e *RemoveFinalizer) IsApplicable(_ context.Context, mrm *v1beta2.ModuleReleaseMeta) (bool, error) { 28 | return controllerutil.ContainsFinalizer(mrm, shared.MandatoryModuleFinalizer), nil 29 | } 30 | 31 | func (e *RemoveFinalizer) Execute(ctx context.Context, mrm *v1beta2.ModuleReleaseMeta) error { 32 | return e.repo.RemoveFinalizer(ctx, mrm.Name, shared.MandatoryModuleFinalizer) 33 | } 34 | -------------------------------------------------------------------------------- /tests/e2e/commontestutils/statefulset.go: -------------------------------------------------------------------------------- 1 | package commontestutils 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | apiappsv1 "k8s.io/api/apps/v1" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | 11 | "github.com/kyma-project/lifecycle-manager/pkg/testutils" 12 | "github.com/kyma-project/lifecycle-manager/pkg/util" 13 | ) 14 | 15 | var ErrStatefulSetNotReady = errors.New("statefulset is not ready") 16 | 17 | func StatefulSetIsReady(ctx context.Context, clnt client.Client, name, namespace string) error { 18 | statefulSet, err := GetStatefulSet(ctx, clnt, name, namespace) 19 | if err != nil { 20 | if util.IsNotFound(err) { 21 | return testutils.ErrNotFound 22 | } 23 | return fmt.Errorf("could not get statefulset: %w", err) 24 | } 25 | 26 | if statefulSet.Spec.Replicas != nil && 27 | *statefulSet.Spec.Replicas == statefulSet.Status.ReadyReplicas { 28 | return nil 29 | } 30 | return ErrStatefulSetNotReady 31 | } 32 | 33 | func GetStatefulSet(ctx context.Context, clnt client.Client, 34 | name, namespace string, 35 | ) (*apiappsv1.StatefulSet, error) { 36 | statefulSet := &apiappsv1.StatefulSet{} 37 | if err := clnt.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, statefulSet); err != nil { 38 | return nil, fmt.Errorf("could not get statefulset: %w", err) 39 | } 40 | return statefulSet, nil 41 | } 42 | -------------------------------------------------------------------------------- /config/gardener-certmanager/certificate_watcher.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cert.gardener.cloud/v1alpha1 2 | kind: Issuer 3 | metadata: 4 | name: watcher-root 5 | namespace: default 6 | spec: 7 | selfSigned: { } 8 | --- 9 | apiVersion: cert.gardener.cloud/v1alpha1 10 | kind: Certificate 11 | metadata: 12 | name: watcher-serving 13 | namespace: istio-system 14 | spec: 15 | dnsNames: 16 | - 'listener.kyma.cloud.sap' # this dnsName should be overwritten based on deployment environment, i.e. listener.dev.kyma.cloud.sap 17 | isCA: true 18 | commonName: klm-watcher-selfsigned-ca 19 | secretName: klm-watcher # this secret will not be prefixed, since it's not managed by kustomize 20 | secretLabels: 21 | operator.kyma-project.io/managed-by: "lifecycle-manager" 22 | privateKey: 23 | algorithm: RSA 24 | size: 4096 25 | issuerRef: 26 | name: watcher-root 27 | namespace: default 28 | duration: 2160h # 90d 29 | renewBefore: 1464h # 61d 30 | --- 31 | apiVersion: cert.gardener.cloud/v1alpha1 32 | kind: Issuer 33 | metadata: 34 | labels: 35 | operator.kyma-project.io/purpose: "klm-watcher-cert-manager" 36 | operator.kyma-project.io/managed-by: "lifecycle-manager" 37 | name: watcher-selfsigned 38 | namespace: default 39 | spec: 40 | ca: 41 | privateKeySecretRef: 42 | name: klm-watcher 43 | namespace: istio-system 44 | -------------------------------------------------------------------------------- /config/certmanager/certificate_webhook.yaml: -------------------------------------------------------------------------------- 1 | # The following manifests contain a self-signed issuer CR and a certificate CR. 2 | # More document can be found at https://docs.cert-manager.io 3 | # WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes. 4 | apiVersion: cert-manager.io/v1 5 | kind: Issuer 6 | metadata: 7 | name: controller-manager-selfsigned 8 | namespace: kcp-system 9 | spec: 10 | selfSigned: { } 11 | --- 12 | apiVersion: cert-manager.io/v1 13 | kind: Certificate 14 | metadata: 15 | name: controller-manager-webhook-serving 16 | namespace: kcp-system 17 | spec: 18 | commonName: klm-controller-manager-webhook-serving 19 | subject: 20 | organizationalUnits: 21 | - "BTP Kyma Runtime" 22 | organizations: 23 | - "SAP SE" 24 | localities: 25 | - "Walldorf" 26 | provinces: 27 | - "Baden-Württemberg" 28 | countries: 29 | - "DE" 30 | # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize 31 | dnsNames: 32 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc 33 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local 34 | issuerRef: 35 | kind: Issuer 36 | name: controller-manager-selfsigned 37 | secretName: klm-controller-manager-webhook # this secret will not be prefixed, since it's not managed by kustomize 38 | -------------------------------------------------------------------------------- /internal/event/event.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | apicorev1 "k8s.io/api/core/v1" 5 | machineryruntime "k8s.io/apimachinery/pkg/runtime" 6 | "k8s.io/client-go/tools/record" 7 | ) 8 | 9 | type Event interface { 10 | Normal(object machineryruntime.Object, reason Reason, msg string) 11 | Warning(object machineryruntime.Object, reason Reason, err error) 12 | } 13 | 14 | type Reason string 15 | 16 | const ( 17 | maxErrorLength int = 100 18 | ) 19 | 20 | type RecorderWrapper struct { 21 | recorder record.EventRecorder 22 | } 23 | 24 | func NewRecorderWrapper(recorder record.EventRecorder) *RecorderWrapper { 25 | return &RecorderWrapper{recorder} 26 | } 27 | 28 | func (e *RecorderWrapper) Normal(obj machineryruntime.Object, reason Reason, msg string) { 29 | if obj == nil { 30 | return 31 | } 32 | e.recorder.Event(obj, apicorev1.EventTypeNormal, string(reason), msg) 33 | } 34 | 35 | func (e *RecorderWrapper) Warning(obj machineryruntime.Object, reason Reason, err error) { 36 | if obj == nil || err == nil { 37 | return 38 | } 39 | e.recorder.Event(obj, apicorev1.EventTypeWarning, string(reason), truncatedErrMsg(err)) 40 | } 41 | 42 | func truncatedErrMsg(err error) string { 43 | msg := err.Error() 44 | length := len(msg) 45 | 46 | if length <= maxErrorLength { 47 | return msg 48 | } 49 | 50 | return msg[length-maxErrorLength:] 51 | } 52 | -------------------------------------------------------------------------------- /cmd/composition/service/mandatorymodule/deletion/deletion_service.go: -------------------------------------------------------------------------------- 1 | package deletion 2 | 3 | import ( 4 | "sigs.k8s.io/controller-runtime/pkg/client" 5 | 6 | "github.com/kyma-project/lifecycle-manager/api/shared" 7 | "github.com/kyma-project/lifecycle-manager/internal/event" 8 | manifestrepo "github.com/kyma-project/lifecycle-manager/internal/repository/manifest" 9 | "github.com/kyma-project/lifecycle-manager/internal/repository/modulereleasemeta" 10 | "github.com/kyma-project/lifecycle-manager/internal/service/mandatorymodule/deletion" 11 | "github.com/kyma-project/lifecycle-manager/internal/service/mandatorymodule/deletion/usecases" 12 | ) 13 | 14 | func ComposeDeletionService(clnt client.Client, eventHandler event.Event) *deletion.Service { 15 | mrmRepo := modulereleasemeta.NewRepository(clnt, shared.DefaultControlPlaneNamespace) 16 | manifestRepo := manifestrepo.NewRepository(clnt, shared.DefaultControlPlaneNamespace) 17 | ensureFinalizerUseCase := usecases.NewEnsureFinalizer(mrmRepo, eventHandler) 18 | skipNonDeletingUseCase := usecases.NewSkipNonDeleting() 19 | deleteManifestsUseCase := usecases.NewDeleteManifests(manifestRepo, eventHandler) 20 | removeFinalizerUseCase := usecases.NewRemoveFinalizer(mrmRepo) 21 | 22 | return deletion.NewService(ensureFinalizerUseCase, skipNonDeletingUseCase, deleteManifestsUseCase, 23 | removeFinalizerUseCase) 24 | } 25 | -------------------------------------------------------------------------------- /.github/actions/setup-private-registry/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup private registry 2 | description: Creates and configures a private registry. 3 | runs: 4 | using: composite 5 | steps: 6 | - name: generate-credentials 7 | shell: bash 8 | run: | 9 | mkdir -p ~/registry-auth 10 | docker run --rm httpd:2.4-alpine htpasswd -Bbn myuser mypass > ~/registry-auth/htpasswd 11 | 12 | - name: run-auth-registry 13 | shell: bash 14 | run: | 15 | docker run -d \ 16 | --restart=always \ 17 | -p [::]:5001:5000 \ 18 | --name private-oci-reg.localhost \ 19 | -v ~/registry-auth:/auth \ 20 | -e "REGISTRY_AUTH=htpasswd" \ 21 | -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \ 22 | -e "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd" \ 23 | registry:2 24 | 25 | - name: connect-to-k3d-network 26 | shell: bash 27 | run: | 28 | docker network connect k3d-kcp private-oci-reg.localhost 29 | 30 | - name: create-and-apply-secret 31 | shell: bash 32 | run: | 33 | kubectl create secret docker-registry private-oci-reg-creds \ 34 | --docker-server=http://private-oci-reg.localhost:5000 \ 35 | --docker-username=myuser \ 36 | --docker-password=mypass \ 37 | --docker-email=dummy@example.com \ 38 | -n kcp-system 39 | -------------------------------------------------------------------------------- /.github/DISCUSSION_TEMPLATE/q-a.yml: -------------------------------------------------------------------------------- 1 | title: "[Question] " 2 | labels: [ "question" ] 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Hello! Thanks for reaching out to us with your question! 8 | Before you submit the question, please check other discussions and the FAQ page. Perhaps your question was already answered! 9 | - type: textarea 10 | id: question 11 | attributes: 12 | label: Question 13 | description: Type your question here. 14 | placeholder: What would you like to ask? 15 | validations: 16 | required: true 17 | - type: markdown 18 | attributes: 19 | value: | 20 | Is there anything that could help us answer the question? Perhaps there are resources or screenshots that could provide additional context? 21 | - type: textarea 22 | id: attachments 23 | attributes: 24 | label: Attachments 25 | description: Attach any files, links, code samples, or screenshots that will give us any additional context. 26 | placeholder: Put your attachments in here. 27 | validations: 28 | required: false 29 | - type: dropdown 30 | id: faq 31 | attributes: 32 | label: Should this question be covered by our FAQ? 33 | multiple: false 34 | options: 35 | - "No" 36 | - "Yes" 37 | validations: 38 | required: false 39 | -------------------------------------------------------------------------------- /internal/descriptor/types/descriptor_test.go: -------------------------------------------------------------------------------- 1 | package types_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | "ocm.software/ocm/api/ocm/compdesc" 9 | compdescv2 "ocm.software/ocm/api/ocm/compdesc/versions/v2" 10 | 11 | "github.com/kyma-project/lifecycle-manager/internal/descriptor/types" 12 | ) 13 | 14 | const ( 15 | testComponentName = "kyma-project.io/test-component" 16 | testComponentVersion = "1.2.3" 17 | ) 18 | 19 | func TestDeserialize(t *testing.T) { 20 | compdesc.RegisterScheme(&compdescv2.DescriptorVersion{}) 21 | 22 | t.Run("Deserialize valid component descriptor should succeed", func(t *testing.T) { 23 | cd := compdesc.New(testComponentName, testComponentVersion) 24 | cdBytes, err := compdesc.Encode(cd) 25 | require.NoError(t, err) 26 | 27 | compDesc, err := types.Deserialize(cdBytes) 28 | require.NoError(t, err) 29 | assert.Equal(t, testComponentName, compDesc.Name) 30 | assert.Equal(t, testComponentVersion, compDesc.Version) 31 | }) 32 | 33 | t.Run("Deserialize invalid component descriptor should return an error", func(t *testing.T) { 34 | invalidCompDescBytes := []byte("invalid component descriptor content") 35 | 36 | _, err := types.Deserialize(invalidCompDescBytes) 37 | require.Error(t, err) 38 | assert.ErrorIs(t, err, types.ErrDecode) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /api-version-compatibility-config.yaml: -------------------------------------------------------------------------------- 1 | # The configuration file is used by the GitHub Actions workflow defined in .github/workflows/check-api-compatibility.yaml. 2 | # It is used to define exclusions when comparing different versions of CRD schemas and 3 | # specifies the fields that should be excluded from the comparison for each version of a CRD. 4 | # The structure of the file is as follows: 5 | # 6 | # : 7 | # exclusions: 8 | # : 9 | # - (may be a yq operator without spaces) 10 | 11 | operator.kyma-project.io_kymas.yaml: 12 | exclusions: 13 | v1beta1: 14 | - .spec.properties.sync 15 | v1beta2: 16 | - .spec.properties.modules.x-kubernetes-list-map-keys 17 | - .spec.properties.modules.x-kubernetes-list-type 18 | operator.kyma-project.io_moduletemplates.yaml: 19 | exclusions: 20 | v1beta1: 21 | - .spec.properties.target 22 | - .spec.required[]|select(.=="target") 23 | v1beta2: 24 | - .spec.properties.version 25 | - .spec.properties.moduleName 26 | - .spec.properties.customStateCheck.description 27 | - .spec.properties.resources 28 | - .spec.properties.info 29 | - .spec.properties.manager 30 | - .spec.properties.associatedResources 31 | operator.kyma-project.io_manifests.yaml: 32 | exclusions: 33 | v1beta2: 34 | - .spec.properties.localizedImages 35 | -------------------------------------------------------------------------------- /internal/service/kyma/deletion/usecases/drop_kyma_finalizer.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | 6 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 7 | 8 | "github.com/kyma-project/lifecycle-manager/api/shared" 9 | "github.com/kyma-project/lifecycle-manager/api/v1beta2" 10 | "github.com/kyma-project/lifecycle-manager/internal/result" 11 | "github.com/kyma-project/lifecycle-manager/internal/result/kyma/usecase" 12 | ) 13 | 14 | const kymaFinalizer = shared.KymaFinalizer 15 | 16 | type KymaRepo interface { 17 | DropFinalizer(ctx context.Context, kymaName string, finalizer string) error 18 | } 19 | 20 | type DropKymaFinalizer struct { 21 | kymaRepo KymaRepo 22 | } 23 | 24 | func NewDropKymaFinalizer(kymaRepo KymaRepo) *DropKymaFinalizer { 25 | return &DropKymaFinalizer{ 26 | kymaRepo: kymaRepo, 27 | } 28 | } 29 | 30 | func (u *DropKymaFinalizer) IsApplicable(ctx context.Context, kcpKyma *v1beta2.Kyma) (bool, error) { 31 | return controllerutil.ContainsFinalizer(kcpKyma, kymaFinalizer), nil 32 | } 33 | 34 | func (u *DropKymaFinalizer) Execute(ctx context.Context, kcpKyma *v1beta2.Kyma) result.Result { 35 | return result.Result{ 36 | UseCase: u.Name(), 37 | Err: u.kymaRepo.DropFinalizer(ctx, kcpKyma.GetName(), kymaFinalizer), 38 | } 39 | } 40 | 41 | func (u *DropKymaFinalizer) Name() result.UseCase { 42 | return usecase.DropKymaFinalizer 43 | } 44 | -------------------------------------------------------------------------------- /internal/util/collections/diffcalc.go: -------------------------------------------------------------------------------- 1 | package collections 2 | 3 | // DiffCalc calculates the difference between two slices of objects for which a string-based identity can be defined. 4 | // The provided identity function should return the smallest unique string representation of the object. 5 | // Note: For most types one can find a unique string identity, 6 | // but for some types it might not be possible or practical: one example would be a built-in map[k]v type. 7 | // In such cases, the DiffCalc type should not be used. 8 | type DiffCalc[E any] struct { 9 | First []E 10 | Identity func(E) string 11 | } 12 | 13 | // notExistingIn returns a slice of object that are present in the first slice but not in the second. 14 | // The returned list is a slice of pointers to the objects in the first slice, to avoid copying the objects. 15 | func (d *DiffCalc[E]) NotExistingIn(second []E) []*E { 16 | onlyInFirst := make([]*E, 0, len(d.First)) 17 | 18 | // Prepare presentInSecond map for diff calculation. 19 | presentInSecond := make(map[string]struct{}, len(second)) 20 | for i := range second { 21 | presentInSecond[d.Identity(second[i])] = struct{}{} 22 | } 23 | // Calculate diff 24 | for i := range d.First { 25 | if _, isInSecond := presentInSecond[d.Identity(d.First[i])]; !isInSecond { 26 | onlyInFirst = append(onlyInFirst, &(d.First[i])) 27 | } 28 | } 29 | return onlyInFirst 30 | } 31 | -------------------------------------------------------------------------------- /internal/service/skrclient/cache/client_cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "crypto/rand" 5 | "math/big" 6 | "time" 7 | 8 | "github.com/jellydator/ttlcache/v3" 9 | 10 | "github.com/kyma-project/lifecycle-manager/internal/service/skrclient" 11 | ) 12 | 13 | const ( 14 | // TTL is between 23 and 25 hours. 15 | ttlInSecondsLower, ttlInSecondsUpper = 23 * 60 * 60, 25 * 60 * 60 16 | ) 17 | 18 | type Service struct { 19 | internal *ttlcache.Cache[string, *skrclient.SKRClient] 20 | } 21 | 22 | func NewService() *Service { 23 | cache := &Service{internal: ttlcache.New[string, *skrclient.SKRClient]()} 24 | go cache.internal.Start() 25 | return cache 26 | } 27 | 28 | func (m *Service) GetClient(key string) *skrclient.SKRClient { 29 | cachedClient := m.internal.Get(key) 30 | if cachedClient != nil { 31 | return cachedClient.Value() 32 | } 33 | return nil 34 | } 35 | 36 | func (m *Service) AddClient(key string, value *skrclient.SKRClient) { 37 | m.internal.Set(key, value, getRandomTTL()) 38 | } 39 | 40 | func (m *Service) DeleteClient(key string) { 41 | m.internal.Delete(key) 42 | } 43 | 44 | func (m *Service) Size() int { 45 | return m.internal.Len() 46 | } 47 | 48 | func getRandomTTL() time.Duration { 49 | randomRange, _ := rand.Int(rand.Reader, big.NewInt(int64(ttlInSecondsUpper-ttlInSecondsLower))) 50 | return time.Duration(randomRange.Int64()+int64(ttlInSecondsLower)) * time.Second 51 | } 52 | -------------------------------------------------------------------------------- /internal/service/kyma/deletion/usecases/set_status_deleting.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyma-project/lifecycle-manager/api/shared" 7 | "github.com/kyma-project/lifecycle-manager/api/v1beta2" 8 | "github.com/kyma-project/lifecycle-manager/internal/result" 9 | "github.com/kyma-project/lifecycle-manager/internal/result/kyma/usecase" 10 | ) 11 | 12 | type KymaStatusRepository interface { 13 | UpdateStatusDeleting(ctx context.Context, kyma *v1beta2.Kyma) error 14 | } 15 | 16 | type SetKymaStatusDeletingUseCase struct { 17 | kymaStatusRepo KymaStatusRepository 18 | } 19 | 20 | func NewSetKymaStatusDeletingUseCase(kymaStatusRepo KymaStatusRepository) *SetKymaStatusDeletingUseCase { 21 | return &SetKymaStatusDeletingUseCase{ 22 | kymaStatusRepo: kymaStatusRepo, 23 | } 24 | } 25 | 26 | func (u *SetKymaStatusDeletingUseCase) IsApplicable(ctx context.Context, kyma *v1beta2.Kyma) (bool, error) { 27 | return !kyma.DeletionTimestamp.IsZero() && kyma.Status.State != shared.StateDeleting, nil 28 | } 29 | 30 | func (u *SetKymaStatusDeletingUseCase) Execute(ctx context.Context, kyma *v1beta2.Kyma) result.Result { 31 | err := u.kymaStatusRepo.UpdateStatusDeleting(ctx, kyma) 32 | return result.Result{ 33 | UseCase: u.Name(), 34 | Err: err, 35 | } 36 | } 37 | 38 | func (u *SetKymaStatusDeletingUseCase) Name() result.UseCase { 39 | return usecase.SetKcpKymaStateDeleting 40 | } 41 | -------------------------------------------------------------------------------- /internal/repository/watcher/certificate/gcm/renewal/certificate_repo.go: -------------------------------------------------------------------------------- 1 | package renewal 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | gcertv1alpha1 "github.com/gardener/cert-management/pkg/apis/cert/v1alpha1" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | ) 11 | 12 | var ErrNilCertificate = errors.New("failed to update nil Certificate") 13 | 14 | type Repository struct { 15 | kcpClient client.Client 16 | namespace string 17 | } 18 | 19 | func NewRepository(kcpClient client.Client, namespace string) *Repository { 20 | return &Repository{ 21 | kcpClient: kcpClient, 22 | namespace: namespace, 23 | } 24 | } 25 | 26 | func (r *Repository) Get(ctx context.Context, name string) (*gcertv1alpha1.Certificate, error) { 27 | cert := &gcertv1alpha1.Certificate{} 28 | cert.SetName(name) 29 | cert.SetNamespace(r.namespace) 30 | 31 | if err := r.kcpClient.Get(ctx, client.ObjectKeyFromObject(cert), cert); err != nil { 32 | return nil, fmt.Errorf("failed to get GCM Certificate %s-%s: %w", name, r.namespace, err) 33 | } 34 | 35 | return cert, nil 36 | } 37 | 38 | func (r *Repository) Update(ctx context.Context, cert *gcertv1alpha1.Certificate) error { 39 | if cert == nil { 40 | return ErrNilCertificate 41 | } 42 | 43 | if err := r.kcpClient.Update(ctx, cert); err != nil { 44 | return fmt.Errorf("failed to update GCM Certificate %s-%s: %w", cert.Name, cert.Namespace, err) 45 | } 46 | 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/service/mandatorymodule/deletion/deletion_service.go: -------------------------------------------------------------------------------- 1 | package deletion 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyma-project/lifecycle-manager/api/v1beta2" 7 | ) 8 | 9 | type UseCase interface { 10 | IsApplicable(ctx context.Context, mrm *v1beta2.ModuleReleaseMeta) (bool, error) 11 | Execute(ctx context.Context, mrm *v1beta2.ModuleReleaseMeta) error 12 | } 13 | 14 | type Service struct { 15 | orderedSteps []UseCase 16 | } 17 | 18 | func NewService(ensureFinalizer UseCase, 19 | skipNonDeleting UseCase, 20 | deleteManifests UseCase, 21 | removeFinalizer UseCase, 22 | ) *Service { 23 | return &Service{ 24 | orderedSteps: []UseCase{ 25 | ensureFinalizer, 26 | skipNonDeleting, 27 | deleteManifests, 28 | removeFinalizer, 29 | }, 30 | } 31 | } 32 | 33 | // HandleDeletion processes the deletion of a ModuleReleaseMeta through a series of ordered use cases. 34 | // Returns deletion.ErrMrmNotInDeletingState error if the MRM is not in deleting state, 35 | // which indicates that the controller should not requeue. 36 | func (s *Service) HandleDeletion(ctx context.Context, mrm *v1beta2.ModuleReleaseMeta) error { 37 | // Find the first applicable step and execute it 38 | for _, step := range s.orderedSteps { 39 | isApplicable, err := step.IsApplicable(ctx, mrm) 40 | if err != nil { 41 | return err 42 | } 43 | if isApplicable { 44 | return step.Execute(ctx, mrm) 45 | } 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /cmd/composition/service/mandatorymodule/installation/installation_service.go: -------------------------------------------------------------------------------- 1 | package installation 2 | 3 | import ( 4 | "sigs.k8s.io/controller-runtime/pkg/client" 5 | 6 | "github.com/kyma-project/lifecycle-manager/api/shared" 7 | "github.com/kyma-project/lifecycle-manager/internal/descriptor/provider" 8 | "github.com/kyma-project/lifecycle-manager/internal/manifest/parser" 9 | "github.com/kyma-project/lifecycle-manager/internal/pkg/metrics" 10 | "github.com/kyma-project/lifecycle-manager/internal/repository/modulereleasemeta" 11 | "github.com/kyma-project/lifecycle-manager/internal/repository/moduletemplate" 12 | "github.com/kyma-project/lifecycle-manager/internal/service/mandatorymodule/installation" 13 | "github.com/kyma-project/lifecycle-manager/pkg/module/sync" 14 | ) 15 | 16 | func ComposeInstallationService(clnt client.Client, 17 | descriptorProvider *provider.CachedDescriptorProvider, 18 | ociRegistryHost string, 19 | remoteSyncNamespace string, 20 | metrics *metrics.MandatoryModulesMetrics, 21 | ) *installation.Service { 22 | mrmRepo := modulereleasemeta.NewRepository(clnt, shared.DefaultControlPlaneNamespace) 23 | mtRepo := moduletemplate.NewRepository(clnt, shared.DefaultControlPlaneNamespace) 24 | moduleParser := parser.NewParser(clnt, descriptorProvider, remoteSyncNamespace, ociRegistryHost) 25 | manifestCreator := sync.New(clnt) 26 | return installation.NewService(mrmRepo, mtRepo, moduleParser, manifestCreator, metrics) 27 | } 28 | -------------------------------------------------------------------------------- /internal/manifest/img/layer.go: -------------------------------------------------------------------------------- 1 | package img 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/kyma-project/lifecycle-manager/api/v1beta2" 9 | ) 10 | 11 | var ErrLayerParsing = errors.New("layer could not be parsed") 12 | 13 | type LayerRepresentation interface { 14 | ToInstallRaw() ([]byte, error) 15 | } 16 | 17 | type OCI struct { 18 | Repo string `json:"repo"` 19 | Name string `json:"name"` 20 | Ref string `json:"ref"` 21 | Type string `json:"type"` 22 | } 23 | 24 | func (o *OCI) ToInstallRaw() ([]byte, error) { 25 | bytes, err := json.Marshal(o) 26 | if err != nil { 27 | return nil, fmt.Errorf("failed to marshal to raw bytes: %w", err) 28 | } 29 | return bytes, nil 30 | } 31 | 32 | func (o *OCI) String() string { 33 | return fmt.Sprintf("%s/%s:%s", o.Repo, o.Name, o.Ref) 34 | } 35 | 36 | type ( 37 | LayerType string 38 | Layer struct { 39 | v1beta2.LayerName 40 | LayerRepresentation 41 | } 42 | ) 43 | 44 | type Layers []Layer 45 | 46 | func (l Layer) ConvertToImageSpec(ociRepo string) (*v1beta2.ImageSpec, error) { 47 | ociImage, ok := l.LayerRepresentation.(*OCI) 48 | if !ok { 49 | return nil, fmt.Errorf("%w: not an OCIImage", ErrLayerParsing) 50 | } 51 | 52 | return &v1beta2.ImageSpec{ 53 | Repo: ociRepo, // Note: we override the repo here with an explicit value 54 | Name: ociImage.Name, 55 | Ref: ociImage.Ref, 56 | Type: v1beta2.RefTypeMetadata(ociImage.Type), 57 | }, nil 58 | } 59 | -------------------------------------------------------------------------------- /markdown_heading_capitalization.js: -------------------------------------------------------------------------------- 1 | module.exports = [{ 2 | "names": [ "custom/capitalize-headings" ], 3 | "description": "Heading words longer than 4 characters should be capitalized", 4 | "tags": [ "formatting" ], 5 | "function": function rule(params, onError) { 6 | params.tokens.filter(function filterToken(token) { 7 | return token.type === "heading_open"; 8 | }).forEach(function forToken(heading) { 9 | var headingTokenContent = heading.line.trim(); 10 | var wordsInHeading = headingTokenContent.split(' '); 11 | 12 | for (var i = 0; i < wordsInHeading.length; i++) { 13 | if (wordsInHeading[i].length > 4 && wordsInHeading[i] && 14 | wordsInHeading[i].charAt(0) !== wordsInHeading[i].charAt(0).toUpperCase()) { 15 | var capitalizedWord = wordsInHeading[i].charAt(0).toUpperCase() + wordsInHeading[i].slice(1); 16 | var detailMessage = "Change " + "'" + wordsInHeading[i] + "'" + " to " + "'" + capitalizedWord + "'"; 17 | 18 | onError({ 19 | "lineNumber": heading.lineNumber, 20 | "detail": detailMessage, 21 | "context": headingTokenContent, // Show the whole heading as context 22 | "range": [headingTokenContent.indexOf(wordsInHeading[i]), wordsInHeading[i].length] // Underline the word which needs a change 23 | }); 24 | } 25 | } 26 | }); 27 | } 28 | }]; 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM --platform=$BUILDPLATFORM golang:1.25.5-alpine AS builder 3 | 4 | WORKDIR /lifecycle-manager 5 | # Copy the Go Modules manifests 6 | COPY go.mod go.mod 7 | COPY go.sum go.sum 8 | 9 | # Copy the go source 10 | COPY cmd cmd/ 11 | COPY api api/ 12 | COPY maintenancewindows maintenancewindows/ 13 | COPY internal internal/ 14 | COPY pkg pkg/ 15 | COPY skr-webhook skr-webhook/ 16 | RUN chmod 755 skr-webhook/ 17 | 18 | # cache deps before building and copying source so that we don't need to re-download as much 19 | # and so that source changes don't invalidate our downloaded layer 20 | RUN go mod download 21 | 22 | # Build 23 | # TAG_default_tag comes from image builder: https://github.com/kyma-project/test-infra/tree/main/cmd/image-builder 24 | ARG TAG_default_tag=from_dockerfile 25 | ARG TARGETOS 26 | ARG TARGETARCH 27 | 28 | RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH GOFIPS140=v1.0.0 go build -ldflags="-X 'main.buildVersion=${TAG_default_tag}'" -a -o manager cmd/main.go 29 | 30 | 31 | # Use distroless as minimal base image to package the manager binary 32 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 33 | FROM gcr.io/distroless/static:nonroot 34 | WORKDIR / 35 | 36 | COPY --chown=65532:65532 --from=builder /lifecycle-manager/manager . 37 | COPY --chown=65532:65532 --from=builder /lifecycle-manager/skr-webhook skr-webhook/ 38 | 39 | USER 65532:65532 40 | 41 | ENTRYPOINT ["/manager"] 42 | -------------------------------------------------------------------------------- /internal/util.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | "k8s.io/cli-runtime/pkg/resource" 9 | ) 10 | 11 | const ( 12 | DebugLogLevel = 2 13 | TraceLogLevel = 3 14 | ) 15 | 16 | func ParseManifestToObjects(path string) (ManifestResources, error) { 17 | objects := &ManifestResources{} 18 | builder := resource.NewLocalBuilder(). 19 | Unstructured(). 20 | Path(false, path). 21 | Flatten(). 22 | ContinueOnError() 23 | 24 | result := builder.Do() 25 | 26 | if err := result.Err(); err != nil { 27 | return ManifestResources{}, fmt.Errorf("parse manifest: %w", err) 28 | } 29 | items, err := result.Infos() 30 | if err != nil { 31 | return ManifestResources{}, fmt.Errorf("parse manifest to resource infos: %w", err) 32 | } 33 | countMap := map[string]bool{} 34 | for _, item := range items { 35 | unstructuredItem, ok := item.Object.(*unstructured.Unstructured) 36 | if !ok { 37 | continue 38 | } 39 | id := getID(unstructuredItem) 40 | if countMap[id] { 41 | continue 42 | } 43 | countMap[id] = true 44 | objects.Items = append(objects.Items, unstructuredItem) 45 | } 46 | return *objects, nil 47 | } 48 | 49 | func getID(item *unstructured.Unstructured) string { 50 | return strings.Join([]string{ 51 | item.GetNamespace(), item.GetName(), 52 | item.GroupVersionKind().Group, item.GroupVersionKind().Version, item.GroupVersionKind().Kind, 53 | }, "/") 54 | } 55 | -------------------------------------------------------------------------------- /internal/service/kyma/deletion/usecases/delete_skr_kyma.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyma-project/lifecycle-manager/api/v1beta2" 7 | "github.com/kyma-project/lifecycle-manager/internal/result" 8 | "github.com/kyma-project/lifecycle-manager/internal/result/kyma/usecase" 9 | ) 10 | 11 | type SkrKymaRepo = ExistsDeleteRepo 12 | 13 | type DeleteSkrKyma struct { 14 | skrKymaRepo SkrKymaRepo 15 | skrAccessSecretRepo SkrAccessSecretRepo 16 | } 17 | 18 | func NewDeleteSkrKyma(skrKymaRepo SkrKymaRepo, 19 | skrAccessSecretRepo SkrAccessSecretRepo, 20 | ) *DeleteSkrKyma { 21 | return &DeleteSkrKyma{ 22 | skrKymaRepo: skrKymaRepo, 23 | skrAccessSecretRepo: skrAccessSecretRepo, 24 | } 25 | } 26 | 27 | func (u *DeleteSkrKyma) IsApplicable(ctx context.Context, kcpKyma *v1beta2.Kyma) (bool, error) { 28 | if kcpKyma.DeletionTimestamp.IsZero() { 29 | return false, nil 30 | } 31 | 32 | if exists, err := u.skrAccessSecretRepo.ExistsForKyma(ctx, kcpKyma.GetName()); !exists || err != nil { 33 | return false, err 34 | } 35 | 36 | return u.skrKymaRepo.Exists(ctx, kcpKyma.GetNamespacedName()) 37 | } 38 | 39 | func (u *DeleteSkrKyma) Execute(ctx context.Context, kcpKyma *v1beta2.Kyma) result.Result { 40 | return result.Result{ 41 | UseCase: u.Name(), 42 | Err: u.skrKymaRepo.Delete(ctx, kcpKyma.GetNamespacedName()), 43 | } 44 | } 45 | 46 | func (u *DeleteSkrKyma) Name() result.UseCase { 47 | return usecase.DeleteSkrKyma 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lifecycle Manager 2 | 3 | 4 | [![REUSE status](https://api.reuse.software/badge/github.com/kyma-project/lifecycle-manager)](https://api.reuse.software/info/github.com/kyma-project/lifecycle-manager) 5 | 6 | ## Overview 7 | 8 | [Kyma](https://kyma-project.io/) is an opinionated set of Kubernetes-based modular building blocks that provides enterprise-grade capabilities for developing and running cloud-native applications. As an actively maintained open-source project supported and managed by SAP, Kyma serves as the foundation for SAP BTP, Kyma runtime within the SAP Business Technology Platform (BTP). 9 | 10 | Kyma Lifecycle Manager (KLM) is a crucial component at the core of SAP BTP, Kyma runtime. 11 | 12 | ## Read More 13 | 14 | For more information, see the [Lifecycle Manager](/docs/README.md) documentation in the `/docs` directory, and the respective subfolders. 15 | 16 | * If you're an end user of SAP BTP, Kyma runtime, go to the [`user`](/docs/user/README.md) directory. 17 | * If you're a developer interested in the module's code, go to the [`contributor`](/docs/contributor/README.md) directory. 18 | * If you're a Lifecycle Manager operator, go to the [`operator`](/docs/operator/README.md) directory. 19 | 20 | ## Contributing 21 | 22 | See the [Contributing Rules](CONTRIBUTING.md). 23 | 24 | ## Code of Conduct 25 | 26 | See the [Code of Conduct](CODE_OF_CONDUCT.md) document. 27 | 28 | ## Licensing 29 | 30 | See the [license](LICENSE) file. 31 | -------------------------------------------------------------------------------- /internal/remote/client_cache.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "crypto/rand" 5 | "math/big" 6 | "time" 7 | 8 | "github.com/jellydator/ttlcache/v3" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | ) 11 | 12 | const ( 13 | // TTL is between 23 and 25 hours. 14 | ttlInSecondsLower, ttlInSecondsUpper = 23 * 60 * 60, 25 * 60 * 60 15 | ) 16 | 17 | type ClientCache struct { 18 | internal *ttlcache.Cache[client.ObjectKey, client.Client] 19 | } 20 | 21 | func NewClientCache() *ClientCache { 22 | cache := &ClientCache{internal: ttlcache.New[client.ObjectKey, client.Client]()} 23 | go cache.internal.Start() 24 | return cache 25 | } 26 | 27 | func (c *ClientCache) Get(key client.ObjectKey) client.Client { 28 | cachedClient := c.internal.Get(key) 29 | if cachedClient != nil { 30 | return cachedClient.Value() 31 | } 32 | return nil 33 | } 34 | 35 | func (c *ClientCache) Add(key client.ObjectKey, value client.Client) { 36 | c.internal.Set(key, value, getRandomTTL()) 37 | } 38 | 39 | func (c *ClientCache) Contains(key client.ObjectKey) bool { 40 | return c.internal.Has(key) 41 | } 42 | 43 | func (c *ClientCache) Delete(key client.ObjectKey) { 44 | c.internal.Delete(key) 45 | } 46 | 47 | func (c *ClientCache) Size() int { 48 | return c.internal.Len() 49 | } 50 | 51 | func getRandomTTL() time.Duration { 52 | randomRange, _ := rand.Int(rand.Reader, big.NewInt(int64(ttlInSecondsUpper-ttlInSecondsLower))) 53 | return time.Duration(randomRange.Int64()+int64(ttlInSecondsLower)) * time.Second 54 | } 55 | -------------------------------------------------------------------------------- /internal/controller/watcher/setup.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | ctrl "sigs.k8s.io/controller-runtime" 8 | ctrlruntime "sigs.k8s.io/controller-runtime/pkg/controller" 9 | "sigs.k8s.io/controller-runtime/pkg/predicate" 10 | 11 | "github.com/kyma-project/lifecycle-manager/api/v1beta2" 12 | "github.com/kyma-project/lifecycle-manager/internal/istio" 13 | ) 14 | 15 | const controllerName = "watcher" 16 | 17 | var errRestConfigIsNotSet = errors.New("reconciler rest config is not set") 18 | 19 | func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, options ctrlruntime.Options) error { 20 | if r.RestConfig == nil { 21 | return errRestConfigIsNotSet 22 | } 23 | var err error 24 | r.IstioClient, err = istio.NewIstioClient(r.RestConfig, ctrl.Log.WithName("istioClient")) 25 | if err != nil { 26 | return fmt.Errorf("unable to set istio client for watcher controller: %w", err) 27 | } 28 | 29 | r.VirtualServiceFactory, err = istio.NewVirtualServiceService(r.Scheme) 30 | if err != nil { 31 | return fmt.Errorf("unable to set VirtualService service for watcher controller: %w", err) 32 | } 33 | 34 | if err = ctrl.NewControllerManagedBy(mgr). 35 | For(&v1beta2.Watcher{}). 36 | Named(controllerName). 37 | WithOptions(options). 38 | WithEventFilter(predicate.Or(predicate.GenerationChangedPredicate{}, predicate.LabelChangedPredicate{})). 39 | Complete(r); err != nil { 40 | return fmt.Errorf("failed to setup manager for watcher controller: %w", err) 41 | } 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /docs/operator/README.md: -------------------------------------------------------------------------------- 1 | # Lifecycle Manager Operator Documentation 2 | 3 | > [!NOTE] 4 | > The documentation topics are shared between operators and contributors. Some of the described features are also part of the user documentation. 5 | 6 | * [Lifecycle Manager](../README.md) 7 | * [Architecture](../contributor/01-architecture.md) 8 | * [Lifecycle Manager Components](../contributor/11-components.md) 9 | * [Lifecycle Manager Resources](../contributor/resources/README.md) 10 | * [Kyma](../contributor/resources/01-kyma.md) 11 | * [Manifest](../contributor/resources/02-manifest.md) 12 | * [ModuleTemplate](../contributor/resources/03-moduletemplate.md) 13 | * [Watcher](../contributor/resources/04-watcher.md) 14 | * [ModuleReleaseMeta](../contributor/resources/05-modulereleasemeta.md) 15 | * [Lifecycle Manager Controllers](../contributor/02-controllers.md) 16 | * [Lifecycle Manager Flags](../contributor/12-klm-arguments.md) 17 | * [API Changelog](../contributor/05-api-changelog.md) 18 | * [Lifecycle Manager Metrics](../contributor/09-metrics.md) 19 | * Features 20 | * [Synchronization Between Kyma Control Plane and SAP BTP, Kyma Runtime](../contributor/08-kcp-skr-synchronization.md) 21 | * [Setting Your Module to the Unmanaged and Managed State](../user/02-unmanaging-modules.md) 22 | * [Maintenance Windows](../contributor/10-maintenance-windows.md) 23 | * [Skipping Maintenance Windows](../user/03-skipping-maintenance-windows.md) 24 | * [Creating ModuleTemplate(using modulectl & ocm cli)](../contributor/14-creating-moduletemplate.md) 25 | --------------------------------------------------------------------------------