From 7d026a86e56eae842d32a0e9cef55f5b8587ce6f Mon Sep 17 00:00:00 2001 From: Johannes Aubart Date: Fri, 25 Jul 2025 15:09:44 +0200 Subject: [PATCH 1/8] implement landscaper v2 bridge --- go.mod | 7 +- go.sum | 14 ++-- .../core/apiserver/controller_test.go | 15 ++-- internal/controller/core/apiserver/v2.go | 29 ++++--- .../controller/core/landscaper/controller.go | 23 +++++- internal/controller/core/landscaper/v2.go | 80 +++++++++++++++++++ internal/utils/components/conditions.go | 11 +++ 7 files changed, 145 insertions(+), 34 deletions(-) create mode 100644 internal/controller/core/landscaper/v2.go diff --git a/go.mod b/go.mod index 731111d..550326b 100644 --- a/go.mod +++ b/go.mod @@ -17,8 +17,9 @@ require ( github.com/openmcp-project/control-plane-operator v0.1.10 github.com/openmcp-project/controller-utils v0.13.1 github.com/openmcp-project/mcp-operator/api v0.33.1 - github.com/openmcp-project/openmcp-operator/api v0.7.0 - github.com/openmcp-project/openmcp-operator/lib v0.8.3 + github.com/openmcp-project/openmcp-operator/api v0.9.0 + github.com/openmcp-project/openmcp-operator/lib v0.9.0 + github.com/openmcp-project/service-provider-landscaper v0.4.0 github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.7 github.com/stretchr/testify v1.10.0 @@ -85,7 +86,7 @@ require ( go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.3 // indirect - golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect + golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect golang.org/x/mod v0.26.0 // indirect golang.org/x/net v0.42.0 // indirect golang.org/x/oauth2 v0.29.0 // indirect diff --git a/go.sum b/go.sum index e59440e..e034c35 100644 --- a/go.sum +++ b/go.sum @@ -115,10 +115,12 @@ github.com/openmcp-project/control-plane-operator v0.1.10 h1:5ticEP3llBmIHQkBzkZ github.com/openmcp-project/control-plane-operator v0.1.10/go.mod h1:GNu9LBTPWoE3dKsBo2kS+SeKSLU2qLtu3VjpaznsB2o= github.com/openmcp-project/controller-utils v0.13.1 h1:+06c0bs1BIO+hBsTcuiEK5y8vpDFoZPml59WNm8fagM= github.com/openmcp-project/controller-utils v0.13.1/go.mod h1:Z1ytVshYcgJq3VQVGqkuZsjO/BCr4UYAaVpHl6JSIMI= -github.com/openmcp-project/openmcp-operator/api v0.7.0 h1:DvaMS3xtAvahGOQm9sI26aotupa8XkwZP52HfOhZ9K0= -github.com/openmcp-project/openmcp-operator/api v0.7.0/go.mod h1:TuAq8Fbrzuykxw/h589M8+QfHotwero5MPWVzdFAqkw= -github.com/openmcp-project/openmcp-operator/lib v0.8.3 h1:2bb1zbP6Si7/fUfXT9M5B0xQnd7O4zGJXaUoe9pDmcA= -github.com/openmcp-project/openmcp-operator/lib v0.8.3/go.mod h1:oydIXRZoNDxtI4DI/JBUB08UPzvfdaKLqHvC4S4HXHQ= +github.com/openmcp-project/openmcp-operator/api v0.9.0 h1:Ss2XTHci3QDwtVQ9QCpAez68WPJNHpabpwMTpFYn0NE= +github.com/openmcp-project/openmcp-operator/api v0.9.0/go.mod h1:cqSmw3+8QJ2Va15aeCmISKIqcWnTpPSb8kch6eyfa6U= +github.com/openmcp-project/openmcp-operator/lib v0.9.0 h1:jbuuZ2HlR0T1wO+Ty8AEaswNeoUK7eGUIw+8zEaicJo= +github.com/openmcp-project/openmcp-operator/lib v0.9.0/go.mod h1:ILD0kCivVMZM5G38GJXflN1AXuQZX4wfH1FgNAcrXo0= +github.com/openmcp-project/service-provider-landscaper v0.4.0 h1:H55q5whlcb1fyhY7/dkeigcp9UPXZQj+WDI57XVY4vY= +github.com/openmcp-project/service-provider-landscaper v0.4.0/go.mod h1:5VU8eJ5nA2Kz0Kc4qbmw6ocMNCgLKzDu5mX6w5hOHa8= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -181,8 +183,8 @@ go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc h1:TS73t7x3KarrNd5qAipmspBDS1rkMcgVG/fS1aRb4Rc= -golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= +golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4= +golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= diff --git a/internal/controller/core/apiserver/controller_test.go b/internal/controller/core/apiserver/controller_test.go index 5d0bd09..7ed4edb 100644 --- a/internal/controller/core/apiserver/controller_test.go +++ b/internal/controller/core/apiserver/controller_test.go @@ -27,6 +27,7 @@ import ( "github.com/openmcp-project/controller-utils/pkg/testing" clustersv1alpha1 "github.com/openmcp-project/openmcp-operator/api/clusters/v1alpha1" + commonapi "github.com/openmcp-project/openmcp-operator/api/common" openmcpclusterutils "github.com/openmcp-project/openmcp-operator/lib/utils" gardenv1beta1 "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/core/v1beta1" @@ -383,10 +384,8 @@ var _ = Describe("CO-1153 APIServer Controller", func() { Expect(env.Client(testutils.LaaSCoreCluster).Create(env.Ctx, cluster)).To(Succeed()) cr.Status.Phase = clustersv1alpha1.REQUEST_GRANTED - cr.Status.Cluster = &clustersv1alpha1.NamespacedObjectReference{ - ObjectReference: clustersv1alpha1.ObjectReference{ - Name: cluster.Name, - }, + cr.Status.Cluster = &commonapi.ObjectReference{ + Name: cluster.Name, Namespace: cluster.Namespace, } Expect(env.Client(testutils.LaaSCoreCluster).Status().Update(env.Ctx, cr)).To(Succeed()) @@ -426,11 +425,11 @@ var _ = Describe("CO-1153 APIServer Controller", func() { dummyShootJson, err := json.Marshal(dummyShoot) Expect(err).NotTo(HaveOccurred()) cluster.Status = clustersv1alpha1.ClusterStatus{ - Phase: clustersv1alpha1.CLUSTER_PHASE_READY, ProviderStatus: &runtime.RawExtension{ Raw: dummyShootJson, }, } + cluster.Status.Phase = clustersv1alpha1.CLUSTER_PHASE_READY Expect(env.Client(testutils.LaaSCoreCluster).Status().Update(env.Ctx, cluster)).To(Succeed()) // reconcile again, should now get further @@ -472,10 +471,8 @@ var _ = Describe("CO-1153 APIServer Controller", func() { Expect(env.Client(testutils.LaaSCoreCluster).Create(env.Ctx, access)).To(Succeed()) ar.Status.Phase = clustersv1alpha1.REQUEST_GRANTED - ar.Status.SecretRef = &clustersv1alpha1.NamespacedObjectReference{ - ObjectReference: clustersv1alpha1.ObjectReference{ - Name: access.Name, - }, + ar.Status.SecretRef = &commonapi.ObjectReference{ + Name: access.Name, Namespace: access.Namespace, } Expect(env.Client(testutils.LaaSCoreCluster).Status().Update(env.Ctx, ar)).To(Succeed()) diff --git a/internal/controller/core/apiserver/v2.go b/internal/controller/core/apiserver/v2.go index 828f706..2581862 100644 --- a/internal/controller/core/apiserver/v2.go +++ b/internal/controller/core/apiserver/v2.go @@ -4,8 +4,10 @@ import ( "context" "fmt" "strconv" + "strings" "time" + "github.com/openmcp-project/controller-utils/pkg/collections" "github.com/openmcp-project/controller-utils/pkg/logging" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" @@ -22,6 +24,7 @@ import ( gcpv1alpha1 "github.com/openmcp-project/cluster-provider-gardener/api/core/v1alpha1" clustersv1alpha1 "github.com/openmcp-project/openmcp-operator/api/clusters/v1alpha1" clustersconst "github.com/openmcp-project/openmcp-operator/api/clusters/v1alpha1/constants" + commonapi "github.com/openmcp-project/openmcp-operator/api/common" openmcpclusterutils "github.com/openmcp-project/openmcp-operator/lib/utils" gardenv1beta1 "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/core/v1beta1" @@ -107,7 +110,9 @@ func v2HandleCreateOrUpdate(ctx context.Context, as *openmcpv1alpha1.APIServer, clusterReadyCon.Status = openmcpv1alpha1.ComponentConditionStatusFromBool(cluster.Status.Phase == clustersv1alpha1.CLUSTER_PHASE_READY) if clusterReadyCon.Status != openmcpv1alpha1.ComponentConditionStatusTrue { clusterReadyCon.Reason = cconst.ReasonClusterNotReady - clusterReadyCon.Message = cluster.Status.Message + clusterReadyCon.Message = strings.Join(collections.ProjectSlice(cluster.Status.Conditions, func(con metav1.Condition) string { + return fmt.Sprintf("[%s] %s", con.Reason, con.Message) + }), "\n") if clusterReadyCon.Message == "" { clusterReadyCon.Message = "Cluster is not ready yet, no further information available" } @@ -160,11 +165,10 @@ func v2HandleCreateOrUpdate(ctx context.Context, as *openmcpv1alpha1.APIServer, } else { clusterRequestGrantedCon.Status = openmcpv1alpha1.ComponentConditionStatusFalse clusterRequestGrantedCon.Reason = cconst.ReasonClusterRequestNotGranted - crReason := cr.Status.Reason - crMessage := cr.Status.Message - if crReason == "" { - crReason = "" - } + crReason := cconst.ReasonClusterRequestNotGranted + crMessage := strings.Join(collections.ProjectSlice(cr.Status.Conditions, func(con metav1.Condition) string { + return fmt.Sprintf("[%s] %s", con.Reason, con.Message) + }), "\n") if crMessage == "" { crMessage = "" } @@ -228,11 +232,10 @@ func v2HandleCreateOrUpdate(ctx context.Context, as *openmcpv1alpha1.APIServer, if ar.Status.Phase != clustersv1alpha1.REQUEST_GRANTED && ar.Status.SecretRef == nil { accessRequestGrantedCon.Status = openmcpv1alpha1.ComponentConditionStatusFalse accessRequestGrantedCon.Reason = cconst.ReasonAccessRequestNotGranted - arReason := cr.Status.Reason - arMessage := cr.Status.Message - if arReason == "" { - arReason = "" - } + arReason := cconst.ReasonAccessRequestNotGranted + arMessage := strings.Join(collections.ProjectSlice(ar.Status.Conditions, func(con metav1.Condition) string { + return fmt.Sprintf("[%s] %s", con.Reason, con.Message) + }), "\n") if arMessage == "" { arMessage = "" } @@ -472,11 +475,11 @@ func (m *AccessRequestMutator) MetadataMutator() resources.MetadataMutator { // Mutate implements resources.Mutator. func (m *AccessRequestMutator) Mutate(r *clustersv1alpha1.AccessRequest) error { if m.isClusterRef && r.Spec.ClusterRef == nil { - r.Spec.ClusterRef = &clustersv1alpha1.NamespacedObjectReference{} + r.Spec.ClusterRef = &commonapi.ObjectReference{} r.Spec.ClusterRef.Name = m.refName r.Spec.ClusterRef.Namespace = m.refNamespace } else if !m.isClusterRef && r.Spec.RequestRef == nil { - r.Spec.RequestRef = &clustersv1alpha1.NamespacedObjectReference{} + r.Spec.RequestRef = &commonapi.ObjectReference{} r.Spec.RequestRef.Name = m.refName r.Spec.RequestRef.Namespace = m.refNamespace } diff --git a/internal/controller/core/landscaper/controller.go b/internal/controller/core/landscaper/controller.go index 1f85d84..7aeef7c 100644 --- a/internal/controller/core/landscaper/controller.go +++ b/internal/controller/core/landscaper/controller.go @@ -11,6 +11,7 @@ import ( "github.com/openmcp-project/mcp-operator/internal/utils/apiserver" "github.com/openmcp-project/mcp-operator/internal/utils/components" + mcpocfg "github.com/openmcp-project/mcp-operator/internal/config" "github.com/openmcp-project/mcp-operator/internal/controller/core/landscaper/conversion" lsutils "github.com/openmcp-project/mcp-operator/internal/controller/core/landscaper/utils" @@ -181,12 +182,27 @@ func (r *LandscaperConnector) reconcile(ctx context.Context, req ctrl.Request) c var res ctrl.Result var ready bool var reason string + var v2cons []openmcpv1alpha1.ComponentCondition var errr openmcperrors.ReasonableError old := ls.DeepCopy() - if deleteLandscaper { - res, ready, reason, errr = r.handleDelete(ctx, ls, ld) + if mcpocfg.Config.Architecture.DecideVersion(ls) == openmcpv1alpha1.ArchitectureV2 { + // v2 logic + log.Info("Using v2 logic for APIServer") + if deleteLandscaper { + res, ready, v2cons, errr = r.v2HandleDelete(ctx, ls) + } else { + res, ready, v2cons, errr = r.v2HandleCreateOrUpdate(ctx, ls) + } + if !ready { + reason = cconst.ReasonWaitingForLaaS + } } else { - res, ready, reason, errr = r.handleCreateOrUpdate(ctx, ls, ld, as) + // v1 logic + if deleteLandscaper { + res, ready, reason, errr = r.handleDelete(ctx, ls, ld) + } else { + res, ready, reason, errr = r.handleCreateOrUpdate(ctx, ls, ld, as) + } } errs := openmcperrors.NewReasonableErrorList(errr) @@ -221,6 +237,7 @@ func (r *LandscaperConnector) reconcile(ctx context.Context, req ctrl.Request) c cons[0].Message = fmt.Sprintf("[%s] %s - %s", ld.Status.LastError.Operation, ld.Status.LastError.Reason, ld.Status.LastError.Message) } } + cons = append(cons, v2cons...) return components.ReconcileResult[*openmcpv1alpha1.Landscaper]{OldComponent: old, Component: ls, Result: res, Reason: reason, ReconcileError: errs.Aggregate(), Conditions: cons} } diff --git a/internal/controller/core/landscaper/v2.go b/internal/controller/core/landscaper/v2.go new file mode 100644 index 0000000..4a1439c --- /dev/null +++ b/internal/controller/core/landscaper/v2.go @@ -0,0 +1,80 @@ +package landscaper + +import ( + "context" + "fmt" + "time" + + "github.com/openmcp-project/controller-utils/pkg/collections" + "github.com/openmcp-project/controller-utils/pkg/logging" + + commonapi "github.com/openmcp-project/openmcp-operator/api/common" + openmcpls "github.com/openmcp-project/service-provider-landscaper/api/v1alpha1" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + cconst "github.com/openmcp-project/mcp-operator/api/constants" + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + openmcperrors "github.com/openmcp-project/mcp-operator/api/errors" + "github.com/openmcp-project/mcp-operator/internal/utils/components" +) + +func (r *LandscaperConnector) v2HandleCreateOrUpdate(ctx context.Context, ls *openmcpv1alpha1.Landscaper) (ctrl.Result, bool, []openmcpv1alpha1.ComponentCondition, openmcperrors.ReasonableError) { + lsv2 := &openmcpls.Landscaper{} + lsv2.SetName(ls.Name) + lsv2.SetNamespace(ls.Namespace) + if _, err := ctrl.CreateOrUpdate(ctx, r.CrateClient, lsv2, func() error { + if lsv2.Labels == nil { + lsv2.Labels = map[string]string{} + } + lsv2.Labels[openmcpv1alpha1.V1MCPReferenceLabelName] = ls.Name + lsv2.Labels[openmcpv1alpha1.V1MCPReferenceLabelNamespace] = ls.Namespace + + return nil + }); err != nil { + return ctrl.Result{}, false, nil, openmcperrors.WithReason(fmt.Errorf("error creating or updating Landscaper v2 resource: %w", err), cconst.ReasonCrateClusterInteractionProblem) + } + + ready := lsv2.Status.Phase == commonapi.StatusPhaseReady && lsv2.Status.ObservedGeneration == lsv2.Generation + cons := collections.ProjectSlice(lsv2.Status.Conditions, func(v2con metav1.Condition) openmcpv1alpha1.ComponentCondition { + return components.NewCondition("LSv2_"+v2con.Type, components.ComponentConditionStatusFromMetav1ConditionStatus(v2con.Status), v2con.Reason, v2con.Message) + }) + + return ctrl.Result{}, ready, cons, nil +} + +func (r *LandscaperConnector) v2HandleDelete(ctx context.Context, ls *openmcpv1alpha1.Landscaper) (ctrl.Result, bool, []openmcpv1alpha1.ComponentCondition, openmcperrors.ReasonableError) { + log := logging.FromContextOrPanic(ctx) + + lsv2 := &openmcpls.Landscaper{} + lsv2.SetName(ls.Name) + lsv2.SetNamespace(ls.Namespace) + if err := r.CrateClient.Get(ctx, client.ObjectKeyFromObject(lsv2), lsv2); err != nil { + if !apierrors.IsNotFound(err) { + return ctrl.Result{}, false, nil, openmcperrors.WithReason(fmt.Errorf("error getting Landscaper v2 resource: %w", err), cconst.ReasonCrateClusterInteractionProblem) + } + lsv2 = nil + } + + if lsv2 != nil { + if lsv2.DeletionTimestamp.IsZero() { + log.Info("Deleting Landscaper v2 resource", "resourceName", lsv2.Name, "resourceNamespace", lsv2.Namespace) + if err := r.CrateClient.Delete(ctx, lsv2); err != nil { + return ctrl.Result{}, false, nil, openmcperrors.WithReason(fmt.Errorf("error deleting Landscaper v2 resource: %w", err), cconst.ReasonCrateClusterInteractionProblem) + } + } else { + log.Info("Waiting for Landscaper v2 resource to be deleted", "resourceName", lsv2.Name, "resourceNamespace", lsv2.Namespace) + } + + cons := collections.ProjectSlice(lsv2.Status.Conditions, func(v2con metav1.Condition) openmcpv1alpha1.ComponentCondition { + return components.NewCondition("LSv2_"+v2con.Type, components.ComponentConditionStatusFromMetav1ConditionStatus(v2con.Status), v2con.Reason, v2con.Message) + }) + return ctrl.Result{RequeueAfter: 30 * time.Second}, false, cons, nil + } + + log.Info("Landscaper v2 resource deleted", "resourceName", ls.Name, "resourceNamespace", ls.Namespace) + return ctrl.Result{}, true, nil, nil +} diff --git a/internal/utils/components/conditions.go b/internal/utils/components/conditions.go index 786f308..4dab472 100644 --- a/internal/utils/components/conditions.go +++ b/internal/utils/components/conditions.go @@ -118,6 +118,17 @@ func NewCondition(conType string, status openmcpv1alpha1.ComponentConditionStatu } } +// ComponentConditionStatusFromMetav1ConditionStatus maps a metav1.ConditionStatus to a ComponentConditionStatus. +func ComponentConditionStatusFromMetav1ConditionStatus(status metav1.ConditionStatus) openmcpv1alpha1.ComponentConditionStatus { + switch status { + case metav1.ConditionTrue: + return openmcpv1alpha1.ComponentConditionStatusTrue + case metav1.ConditionFalse: + return openmcpv1alpha1.ComponentConditionStatusFalse + } + return openmcpv1alpha1.ComponentConditionStatusUnknown +} + // IsComponentReady returns true if the component's observedGenerations are up-to-date and all of its relevant conditions are "True". // If relevantConditions is empty, all of the component's conditions are deemed relevant. // Condition types in relevantConditions for which no condition exists on the component are considered "Unknown" and cause the method to return false. From 78a865c0767a470f28b722de882c6a1692fd670d Mon Sep 17 00:00:00 2001 From: Johannes Aubart Date: Fri, 25 Jul 2025 15:37:40 +0200 Subject: [PATCH 2/8] add tests for landscaper v2 bridge logic --- .../controller/core/landscaper/controller.go | 2 +- .../core/landscaper/controller_test.go | 66 ++++++++++++++++--- test/utils/test.go | 2 + 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/internal/controller/core/landscaper/controller.go b/internal/controller/core/landscaper/controller.go index 7aeef7c..41b25fa 100644 --- a/internal/controller/core/landscaper/controller.go +++ b/internal/controller/core/landscaper/controller.go @@ -185,7 +185,7 @@ func (r *LandscaperConnector) reconcile(ctx context.Context, req ctrl.Request) c var v2cons []openmcpv1alpha1.ComponentCondition var errr openmcperrors.ReasonableError old := ls.DeepCopy() - if mcpocfg.Config.Architecture.DecideVersion(ls) == openmcpv1alpha1.ArchitectureV2 { + if mcpocfg.Config.Architecture.Landscaper.Version == openmcpv1alpha1.ArchitectureV2 { // v2 logic log.Info("Using v2 logic for APIServer") if deleteLandscaper { diff --git a/internal/controller/core/landscaper/controller_test.go b/internal/controller/core/landscaper/controller_test.go index 4fb1897..dff2356 100644 --- a/internal/controller/core/landscaper/controller_test.go +++ b/internal/controller/core/landscaper/controller_test.go @@ -3,22 +3,26 @@ package landscaper_test import ( "path" + openmcpls "github.com/openmcp-project/service-provider-landscaper/api/v1alpha1" + "github.com/openmcp-project/mcp-operator/internal/components" + mcpocfg "github.com/openmcp-project/mcp-operator/internal/config" componentutils "github.com/openmcp-project/mcp-operator/internal/utils/components" "github.com/openmcp-project/mcp-operator/internal/controller/core/landscaper" - lssv1alpha1 "github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "k8s.io/apimachinery/pkg/api/errors" + + . "github.com/openmcp-project/mcp-operator/test/matchers" + + lssv1alpha1 "github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" - . "github.com/openmcp-project/mcp-operator/test/matchers" - "github.com/openmcp-project/controller-utils/pkg/testing" cconst "github.com/openmcp-project/mcp-operator/api/constants" @@ -349,11 +353,11 @@ var _ = Describe("CO-1153 Landscaper Controller", func() { // now it should be gone err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls) Expect(err).To(HaveOccurred()) - Expect(errors.IsNotFound(err)).To(BeTrue()) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) err = env.Client(testutils.LaaSCoreCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, &lssv1alpha1.LandscaperDeployment{}) Expect(err).To(HaveOccurred()) - Expect(errors.IsNotFound(err)).To(BeTrue()) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) as := &openmcpv1alpha1.APIServer{} Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as)).To(Succeed()) @@ -437,7 +441,7 @@ var _ = Describe("CO-1153 Landscaper Controller", func() { err = env.Client(testutils.LaaSCoreCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, &lssv1alpha1.LandscaperDeployment{}) Expect(err).To(HaveOccurred()) - Expect(errors.IsNotFound(err)).To(BeTrue()) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) It("should handle when landscaper is not found", func() { @@ -470,7 +474,7 @@ var _ = Describe("CO-1153 Landscaper Controller", func() { err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls) Expect(err).To(HaveOccurred()) - Expect(errors.IsNotFound(err)).To(BeTrue()) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) as := &openmcpv1alpha1.APIServer{} Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as)).To(Succeed()) @@ -484,4 +488,50 @@ var _ = Describe("CO-1153 Landscaper Controller", func() { Expect(componentutils.HasDepedencyFinalizer(authz, lsComp.Type())).To(BeFalse()) }) + Context("v2", func() { + + BeforeEach(func() { + mcpocfg.Config.Architecture.Landscaper.Version = openmcpv1alpha1.ArchitectureV2 + }) + + It("should create a v2 Landscaper object", func() { + env := testEnvSetup(path.Join("testdata", "test-04"), "") + + ls := &openmcpv1alpha1.Landscaper{} + ls.SetName("test") + ls.SetNamespace("test") + err := env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls) + Expect(err).NotTo(HaveOccurred()) + + env.ShouldReconcile(lsReconciler, testing.RequestFromObject(ls)) + + lsv2 := &openmcpls.Landscaper{} + lsv2.SetName(ls.Name) + lsv2.SetNamespace(ls.Namespace) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(lsv2), lsv2)).To(Succeed()) + + // add dummy finalizer to verify that controller waits for the lsv2 object to be deleted + lsv2.Finalizers = append(lsv2.Finalizers, "dummy") + err = env.Client(testutils.CrateCluster).Update(env.Ctx, lsv2) + Expect(err).NotTo(HaveOccurred()) + + Expect(env.Client(testutils.CrateCluster).Delete(env.Ctx, ls)).To(Succeed()) + env.ShouldReconcile(lsReconciler, testing.RequestFromObject(ls)) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls)).To(Succeed()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(lsv2), lsv2)).To(Succeed()) + Expect(ls.DeletionTimestamp.IsZero()).To(BeFalse()) + Expect(lsv2.DeletionTimestamp.IsZero()).To(BeFalse()) + + lsv2.Finalizers = nil + Expect(env.Client(testutils.CrateCluster).Update(env.Ctx, lsv2)).To(Succeed()) + + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls)).To(Succeed()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(lsv2), lsv2)).To(MatchError(apierrors.IsNotFound, "not found")) + + env.ShouldReconcile(lsReconciler, testing.RequestFromObject(ls)) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls)).To(MatchError(apierrors.IsNotFound, "not found")) + }) + + }) + }) diff --git a/test/utils/test.go b/test/utils/test.go index 687cdc3..b522108 100644 --- a/test/utils/test.go +++ b/test/utils/test.go @@ -10,6 +10,7 @@ import ( cocorev1beta1 "github.com/openmcp-project/control-plane-operator/api/v1beta1" "github.com/openmcp-project/controller-utils/pkg/testing" v2install "github.com/openmcp-project/openmcp-operator/api/install" + lsv2install "github.com/openmcp-project/service-provider-landscaper/api/install" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -32,6 +33,7 @@ func init() { utilruntime.Must(gardenauthenticationv1alpha1.AddToScheme(Scheme)) utilruntime.Must(clientgoscheme.AddToScheme(Scheme)) v2install.InstallOperatorAPIs(Scheme) + lsv2install.InstallProviderAPIs(Scheme) } const ( From d73900fd149228dd9b6019d723e24d58094f7453 Mon Sep 17 00:00:00 2001 From: Johannes Aubart Date: Mon, 28 Jul 2025 11:26:04 +0200 Subject: [PATCH 3/8] fix bug --- internal/controller/core/landscaper/controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/controller/core/landscaper/controller.go b/internal/controller/core/landscaper/controller.go index 41b25fa..7aeef7c 100644 --- a/internal/controller/core/landscaper/controller.go +++ b/internal/controller/core/landscaper/controller.go @@ -185,7 +185,7 @@ func (r *LandscaperConnector) reconcile(ctx context.Context, req ctrl.Request) c var v2cons []openmcpv1alpha1.ComponentCondition var errr openmcperrors.ReasonableError old := ls.DeepCopy() - if mcpocfg.Config.Architecture.Landscaper.Version == openmcpv1alpha1.ArchitectureV2 { + if mcpocfg.Config.Architecture.DecideVersion(ls) == openmcpv1alpha1.ArchitectureV2 { // v2 logic log.Info("Using v2 logic for APIServer") if deleteLandscaper { From a3075117d88709ca2041f8fd1356f13a3fd1de33 Mon Sep 17 00:00:00 2001 From: Johannes Aubart Date: Mon, 28 Jul 2025 11:54:14 +0200 Subject: [PATCH 4/8] fix missing scheme --- cmd/mcp-operator/app/app.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/mcp-operator/app/app.go b/cmd/mcp-operator/app/app.go index 0f45214..7ac4876 100644 --- a/cmd/mcp-operator/app/app.go +++ b/cmd/mcp-operator/app/app.go @@ -22,6 +22,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/cluster" v2install "github.com/openmcp-project/openmcp-operator/api/install" + lsv2install "github.com/openmcp-project/service-provider-landscaper/api/install" laasinstall "github.com/gardener/landscaper-service/pkg/apis/core/install" cocorev1beta1 "github.com/openmcp-project/control-plane-operator/api/v1beta1" @@ -290,6 +291,7 @@ func (o *Options) run(ctx context.Context) error { sc := runtime.NewScheme() openmcpinstall.Install(sc) utilruntime.Must(clientgoscheme.AddToScheme(sc)) + lsv2install.InstallProviderAPIs(sc) mgr, err := ctrl.NewManager(o.CrateClusterConfig, ctrl.Options{ Scheme: sc, Metrics: server.Options{ From 385c22ec9c0b869f01c69b6d794908266fb38969 Mon Sep 17 00:00:00 2001 From: Johannes Aubart Date: Tue, 29 Jul 2025 09:08:02 +0200 Subject: [PATCH 5/8] add landscaper architecture config to helm chart --- .../mcp-operator/templates/configmap-mcp-operator-config.yaml | 4 ++++ charts/mcp-operator/values.yaml | 3 +++ 2 files changed, 7 insertions(+) diff --git a/charts/mcp-operator/templates/configmap-mcp-operator-config.yaml b/charts/mcp-operator/templates/configmap-mcp-operator-config.yaml index ec5f50f..226e290 100644 --- a/charts/mcp-operator/templates/configmap-mcp-operator-config.yaml +++ b/charts/mcp-operator/templates/configmap-mcp-operator-config.yaml @@ -18,3 +18,7 @@ data: version: {{ .Values.apiserver.architecture.version | default "v1" }} allowOverride: {{ .Values.apiserver.architecture.allowOverride | default false }} {{- end }} + landscaper: + version: {{ .Values.landscaper.architecture.version | default "v1" }} + allowOverride: {{ .Values.landscaper.architecture.allowOverride | default false }} + {{- end }} diff --git a/charts/mcp-operator/values.yaml b/charts/mcp-operator/values.yaml index 5adfc5c..b3910de 100644 --- a/charts/mcp-operator/values.yaml +++ b/charts/mcp-operator/values.yaml @@ -109,6 +109,9 @@ apiserver: landscaper: disabled: false + # architecture: + # version: v1 + # allowOverride: false clusters: # core: # # specify either kubeconfig or host, audience, and one of caData or caConfigMapName. From 0abce7e6d7dd0e101a32abb4b5e8bb49d83f502f Mon Sep 17 00:00:00 2001 From: Johannes Aubart Date: Tue, 29 Jul 2025 10:42:24 +0200 Subject: [PATCH 6/8] improve debuggability --- api/constants/conditions.go | 3 ++ internal/controller/core/landscaper/v2.go | 36 ++++++++++++++++++++--- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/api/constants/conditions.go b/api/constants/conditions.go index 449aef7..690fd48 100644 --- a/api/constants/conditions.go +++ b/api/constants/conditions.go @@ -9,4 +9,7 @@ const ( ConditionAccessRequestGranted = "AccessRequestGranted" ConditionAccessRequestDeleted = "AccessRequestDeleted" ConditionClusterRequestDeleted = "ClusterRequestDeleted" + + ConditionLandscaperV2ResourceCreatedOrUpdated = "LandscaperV2ResourceCreatedOrUpdated" + ConditionLandscaperV2ResourceDeleted = "LandscaperV2ResourceDeleted" ) diff --git a/internal/controller/core/landscaper/v2.go b/internal/controller/core/landscaper/v2.go index 4a1439c..07eb2ba 100644 --- a/internal/controller/core/landscaper/v2.go +++ b/internal/controller/core/landscaper/v2.go @@ -23,6 +23,11 @@ import ( ) func (r *LandscaperConnector) v2HandleCreateOrUpdate(ctx context.Context, ls *openmcpv1alpha1.Landscaper) (ctrl.Result, bool, []openmcpv1alpha1.ComponentCondition, openmcperrors.ReasonableError) { + log := logging.FromContextOrPanic(ctx) + log.Info("Creating or updating Landscaper v2 resource", "resourceName", ls.Name, "resourceNamespace", ls.Namespace) + + con := components.NewCondition(cconst.ConditionLandscaperV2ResourceCreatedOrUpdated, openmcpv1alpha1.ComponentConditionStatusUnknown, "", "") + lsv2 := &openmcpls.Landscaper{} lsv2.SetName(ls.Name) lsv2.SetNamespace(ls.Namespace) @@ -35,13 +40,19 @@ func (r *LandscaperConnector) v2HandleCreateOrUpdate(ctx context.Context, ls *op return nil }); err != nil { - return ctrl.Result{}, false, nil, openmcperrors.WithReason(fmt.Errorf("error creating or updating Landscaper v2 resource: %w", err), cconst.ReasonCrateClusterInteractionProblem) + rerr := openmcperrors.WithReason(fmt.Errorf("error creating or updating Landscaper v2 resource: %w", err), cconst.ReasonCrateClusterInteractionProblem) + con.Status = openmcpv1alpha1.ComponentConditionStatusFalse + con.Reason = rerr.Reason() + con.Message = rerr.Error() + return ctrl.Result{}, false, []openmcpv1alpha1.ComponentCondition{con}, rerr } ready := lsv2.Status.Phase == commonapi.StatusPhaseReady && lsv2.Status.ObservedGeneration == lsv2.Generation cons := collections.ProjectSlice(lsv2.Status.Conditions, func(v2con metav1.Condition) openmcpv1alpha1.ComponentCondition { return components.NewCondition("LSv2_"+v2con.Type, components.ComponentConditionStatusFromMetav1ConditionStatus(v2con.Status), v2con.Reason, v2con.Message) }) + con.Status = openmcpv1alpha1.ComponentConditionStatusTrue + cons = append(cons, con) return ctrl.Result{}, ready, cons, nil } @@ -49,12 +60,18 @@ func (r *LandscaperConnector) v2HandleCreateOrUpdate(ctx context.Context, ls *op func (r *LandscaperConnector) v2HandleDelete(ctx context.Context, ls *openmcpv1alpha1.Landscaper) (ctrl.Result, bool, []openmcpv1alpha1.ComponentCondition, openmcperrors.ReasonableError) { log := logging.FromContextOrPanic(ctx) + con := components.NewCondition(cconst.ConditionLandscaperV2ResourceDeleted, openmcpv1alpha1.ComponentConditionStatusUnknown, "", "") + lsv2 := &openmcpls.Landscaper{} lsv2.SetName(ls.Name) lsv2.SetNamespace(ls.Namespace) if err := r.CrateClient.Get(ctx, client.ObjectKeyFromObject(lsv2), lsv2); err != nil { if !apierrors.IsNotFound(err) { - return ctrl.Result{}, false, nil, openmcperrors.WithReason(fmt.Errorf("error getting Landscaper v2 resource: %w", err), cconst.ReasonCrateClusterInteractionProblem) + rerr := openmcperrors.WithReason(fmt.Errorf("error getting Landscaper v2 resource: %w", err), cconst.ReasonCrateClusterInteractionProblem) + con.Status = openmcpv1alpha1.ComponentConditionStatusFalse + con.Reason = rerr.Reason() + con.Message = rerr.Error() + return ctrl.Result{}, false, []openmcpv1alpha1.ComponentCondition{con}, rerr } lsv2 = nil } @@ -63,7 +80,11 @@ func (r *LandscaperConnector) v2HandleDelete(ctx context.Context, ls *openmcpv1a if lsv2.DeletionTimestamp.IsZero() { log.Info("Deleting Landscaper v2 resource", "resourceName", lsv2.Name, "resourceNamespace", lsv2.Namespace) if err := r.CrateClient.Delete(ctx, lsv2); err != nil { - return ctrl.Result{}, false, nil, openmcperrors.WithReason(fmt.Errorf("error deleting Landscaper v2 resource: %w", err), cconst.ReasonCrateClusterInteractionProblem) + rerr := openmcperrors.WithReason(fmt.Errorf("error deleting Landscaper v2 resource: %w", err), cconst.ReasonCrateClusterInteractionProblem) + con.Status = openmcpv1alpha1.ComponentConditionStatusFalse + con.Reason = rerr.Reason() + con.Message = rerr.Error() + return ctrl.Result{}, false, []openmcpv1alpha1.ComponentCondition{con}, rerr } } else { log.Info("Waiting for Landscaper v2 resource to be deleted", "resourceName", lsv2.Name, "resourceNamespace", lsv2.Namespace) @@ -72,9 +93,16 @@ func (r *LandscaperConnector) v2HandleDelete(ctx context.Context, ls *openmcpv1a cons := collections.ProjectSlice(lsv2.Status.Conditions, func(v2con metav1.Condition) openmcpv1alpha1.ComponentCondition { return components.NewCondition("LSv2_"+v2con.Type, components.ComponentConditionStatusFromMetav1ConditionStatus(v2con.Status), v2con.Reason, v2con.Message) }) + con.Status = openmcpv1alpha1.ComponentConditionStatusFalse + con.Reason = cconst.ReasonWaitingForLaaS + con.Message = "Waiting for Landscaper v2 resource to be deleted" + cons = append(cons, con) + return ctrl.Result{RequeueAfter: 30 * time.Second}, false, cons, nil } log.Info("Landscaper v2 resource deleted", "resourceName", ls.Name, "resourceNamespace", ls.Namespace) - return ctrl.Result{}, true, nil, nil + con.Status = openmcpv1alpha1.ComponentConditionStatusTrue + + return ctrl.Result{}, true, []openmcpv1alpha1.ComponentCondition{con}, nil } From b4943a56c423b66e79e4d0e395f8cf182d0a78f3 Mon Sep 17 00:00:00 2001 From: Johannes Aubart Date: Tue, 29 Jul 2025 13:12:06 +0200 Subject: [PATCH 7/8] fix docs and apiserver key --- .../templates/configmap-mcp-operator-config.yaml | 2 +- docs/architecture-v2/bridge.md | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/charts/mcp-operator/templates/configmap-mcp-operator-config.yaml b/charts/mcp-operator/templates/configmap-mcp-operator-config.yaml index 226e290..9ca0d9b 100644 --- a/charts/mcp-operator/templates/configmap-mcp-operator-config.yaml +++ b/charts/mcp-operator/templates/configmap-mcp-operator-config.yaml @@ -14,7 +14,7 @@ data: {{- .Values.architecture.immutability | toYaml | nindent 8 }} {{- end }} {{- if and .Values.apiserver .Values.apiserver.architecture }} - apiserver: + apiServer: version: {{ .Values.apiserver.architecture.version | default "v1" }} allowOverride: {{ .Values.apiserver.architecture.allowOverride | default false }} {{- end }} diff --git a/docs/architecture-v2/bridge.md b/docs/architecture-v2/bridge.md index bb94fd3..51e7911 100644 --- a/docs/architecture-v2/bridge.md +++ b/docs/architecture-v2/bridge.md @@ -4,6 +4,7 @@ In order to migrate an existing MCP landscape to the new v2 architecture step by The bridge is currently implemented for the following components: - `APIServer` +- `Landscaper` ## Architecture Configuration @@ -12,7 +13,10 @@ To configure for which components the bridge is enabled, set the architecture co immutability: policyName: mcp-architecture-immutability disabled: false -apiServer: +apiserver: + version: v1 + allowOverride: false +landscaper: version: v1 allowOverride: false # more components are to follow From e1e9d475937296c6e8958f81584c85d0540bf9b9 Mon Sep 17 00:00:00 2001 From: Johannes Aubart Date: Tue, 29 Jul 2025 13:31:48 +0200 Subject: [PATCH 8/8] fix rbac --- charts/mcp-operator/templates/rbac.yaml | 66 ++++++++++++++----------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/charts/mcp-operator/templates/rbac.yaml b/charts/mcp-operator/templates/rbac.yaml index a9f46d7..3b1e113 100644 --- a/charts/mcp-operator/templates/rbac.yaml +++ b/charts/mcp-operator/templates/rbac.yaml @@ -116,6 +116,43 @@ roleRef: name: {{ include "mcp-operator.landscaper.clusterrole" . }} apiGroup: rbac.authorization.k8s.io --- +apiVersion: {{ include "rbacversion" . }} +kind: ClusterRole +metadata: + name: {{ include "mcp-operator.v2bridge.clusterrole" . }} + labels: + {{- include "mcp-operator.labels" . | nindent 4 }} +rules: +- apiGroups: + - clusters.openmcp.cloud + resources: + - "*" + verbs: + - "*" +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch +--- +kind: ClusterRoleBinding +apiVersion: {{ include "rbacversion" . }} +metadata: + name: {{ include "mcp-operator.v2bridge.clusterrole" . }} + labels: + {{- include "mcp-operator.labels" . | nindent 4 }} +subjects: +- kind: ServiceAccount + name: mcp-operator + namespace: {{ .Release.Namespace }} +roleRef: + kind: ClusterRole + name: {{ include "mcp-operator.v2bridge.clusterrole" . }} + apiGroup: rbac.authorization.k8s.io +--- {{- end }} {{- end }} {{- if has "cloudorchestrator" ( include "mcp-operator.activeControllers" .Values | fromYamlArray ) }} @@ -181,32 +218,3 @@ subjects: namespace: {{ .Release.Namespace }} --- {{- end }} -apiVersion: {{ include "rbacversion" . }} -kind: ClusterRole -metadata: - name: {{ include "mcp-operator.v2bridge.clusterrole" . }} - labels: - {{- include "mcp-operator.labels" . | nindent 4 }} -rules: -- apiGroups: - - clusters.openmcp.cloud - resources: - - "*" - verbs: - - "*" ---- -kind: ClusterRoleBinding -apiVersion: {{ include "rbacversion" . }} -metadata: - name: {{ include "mcp-operator.v2bridge.clusterrole" . }} - labels: - {{- include "mcp-operator.labels" . | nindent 4 }} -subjects: -- kind: ServiceAccount - name: mcp-operator - namespace: {{ .Release.Namespace }} -roleRef: - kind: ClusterRole - name: {{ include "mcp-operator.v2bridge.clusterrole" . }} - apiGroup: rbac.authorization.k8s.io ----