From 557a78894c85b66e3cde467e0244815f8ea35c30 Mon Sep 17 00:00:00 2001 From: Mangirdas Judeikis Date: Tue, 30 Dec 2025 09:31:15 +0200 Subject: [PATCH 1/5] Rename clusterscopeisolation to isolation Signed-off-by: Mangirdas Judeikis On-behalf-of: @SAP mangirdas.judeikis@sap.com --- .../serviceexportrequest_controller.go | 20 ++++----- .../serviceexportrequest_reconcile.go | 16 +++---- .../servicenamespace_controller.go | 14 +++--- backend/oidc/oidc_test.go | 2 + backend/options/options.go | 45 ++++++++++--------- backend/server.go | 4 +- .../resources/apiexport-kube-bind.io.yaml | 2 +- ...schema-apiserviceexports.kube-bind.io.yaml | 19 +++++++- .../crds/kube-bind.io_apiserviceexports.yaml | 17 +++++++ .../charts/backend/templates/deployment.yaml | 4 +- .../crd/kube-bind.io_apiserviceexports.yaml | 17 +++++++ docs/content/setup/kcp-setup.md | 3 +- docs/content/usage/synchronization.md | 4 +- .../v1alpha2/apiserviceexport_types.go | 10 +++++ test/e2e/bind/happy-case_test.go | 2 +- 15 files changed, 121 insertions(+), 58 deletions(-) diff --git a/backend/controllers/serviceexportrequest/serviceexportrequest_controller.go b/backend/controllers/serviceexportrequest/serviceexportrequest_controller.go index 3788d011b..212aa49f8 100644 --- a/backend/controllers/serviceexportrequest/serviceexportrequest_controller.go +++ b/backend/controllers/serviceexportrequest/serviceexportrequest_controller.go @@ -49,9 +49,9 @@ type APIServiceExportRequestReconciler struct { manager mcmanager.Manager opts controller.TypedOptions[mcreconcile.Request] - informerScope kubebindv1alpha2.InformerScope - clusterScopedIsolation kubebindv1alpha2.Isolation - reconciler reconciler + informerScope kubebindv1alpha2.InformerScope + isolation kubebindv1alpha2.Isolation + reconciler reconciler } // NewAPIServiceExportRequestReconciler returns a new APIServiceExportRequestReconciler to reconcile APIServiceExportRequests. @@ -75,14 +75,14 @@ func NewAPIServiceExportRequestReconciler( } r := &APIServiceExportRequestReconciler{ - manager: mgr, - opts: opts, - informerScope: scope, - clusterScopedIsolation: isolation, + manager: mgr, + opts: opts, + informerScope: scope, + isolation: isolation, reconciler: reconciler{ - informerScope: scope, - clusterScopedIsolation: isolation, - schemaSource: schemaSource, + informerScope: scope, + isolation: isolation, + schemaSource: schemaSource, getBoundSchema: func(ctx context.Context, cl client.Client, namespace, name string) (*kubebindv1alpha2.BoundSchema, error) { var schema kubebindv1alpha2.BoundSchema key := types.NamespacedName{Namespace: namespace, Name: name} diff --git a/backend/controllers/serviceexportrequest/serviceexportrequest_reconcile.go b/backend/controllers/serviceexportrequest/serviceexportrequest_reconcile.go index 42533b75f..617c09930 100644 --- a/backend/controllers/serviceexportrequest/serviceexportrequest_reconcile.go +++ b/backend/controllers/serviceexportrequest/serviceexportrequest_reconcile.go @@ -40,9 +40,9 @@ import ( ) type reconciler struct { - informerScope kubebindv1alpha2.InformerScope - clusterScopedIsolation kubebindv1alpha2.Isolation - schemaSource string + informerScope kubebindv1alpha2.InformerScope + isolation kubebindv1alpha2.Isolation + schemaSource string getBoundSchema func(ctx context.Context, cl client.Client, namespace, name string) (*kubebindv1alpha2.BoundSchema, error) createBoundSchema func(ctx context.Context, cl client.Client, schema *kubebindv1alpha2.BoundSchema) error @@ -134,7 +134,7 @@ func (r *reconciler) getExportedSchemas(ctx context.Context, cl client.Client) ( return boundSchemas, nil } -func (r *reconciler) ensureBoundSchemas(ctx context.Context, cl client.Client, cache cache.Cache, req *kubebindv1alpha2.APIServiceExportRequest) error { +func (r *reconciler) ensureBoundSchemas(ctx context.Context, cl client.Client, _ cache.Cache, req *kubebindv1alpha2.APIServiceExportRequest) error { exportedSchemas, err := r.getExportedSchemas(ctx, cl) if err != nil { return err @@ -167,7 +167,7 @@ func (r *reconciler) ensureBoundSchemas(ctx context.Context, cl client.Client, c // we need to rewrite the BoundSchema's scope accordingly. For all // other isolation strategies, as well as for namespaced schemas, // no changes are necessary. - if boundSchema.Spec.Scope == apiextensionsv1.NamespaceScoped && r.clusterScopedIsolation == kubebindv1alpha2.IsolationNamespaced { + if boundSchema.Spec.Scope == apiextensionsv1.NamespaceScoped && r.isolation == kubebindv1alpha2.IsolationNamespaced { boundSchema.Spec.Scope = apiextensionsv1.ClusterScoped } @@ -185,7 +185,6 @@ func (r *reconciler) ensureExports(ctx context.Context, cl client.Client, cache logger := klog.FromContext(ctx) var schemas []*kubebindv1alpha2.BoundSchema - var scope apiextensionsv1.ResourceScope if req.Status.Phase == kubebindv1alpha2.APIServiceExportRequestPhasePending { for _, res := range req.Spec.Resources { name := res.ResourceGroupName() @@ -207,7 +206,6 @@ func (r *reconciler) ensureExports(ctx context.Context, cl client.Client, cache // Collect all schemas for hashing. // TODO(mjudeikis) Scope is same for all crds so we keep stamping it over. We might want to change this - scope = boundSchema.Spec.Scope schemas = append(schemas, boundSchema) } @@ -236,11 +234,9 @@ func (r *reconciler) ensureExports(ctx context.Context, cl client.Client, cache }, Spec: kubebindv1alpha2.APIServiceExportSpec{ InformerScope: r.informerScope, + Isolation: r.isolation, }, } - if scope == apiextensionsv1.ClusterScoped { - export.Spec.ClusterScopedIsolation = r.clusterScopedIsolation - } for _, res := range req.Spec.Resources { export.Spec.Resources = append(export.Spec.Resources, kubebindv1alpha2.APIServiceExportResource{ diff --git a/backend/controllers/servicenamespace/servicenamespace_controller.go b/backend/controllers/servicenamespace/servicenamespace_controller.go index 0b28901b9..20981ed8f 100644 --- a/backend/controllers/servicenamespace/servicenamespace_controller.go +++ b/backend/controllers/servicenamespace/servicenamespace_controller.go @@ -50,9 +50,9 @@ type APIServiceNamespaceReconciler struct { manager mcmanager.Manager opts controller.TypedOptions[mcreconcile.Request] - informerScope kubebindv1alpha2.InformerScope - clusterScopedIsolation kubebindv1alpha2.Isolation - reconciler reconciler + informerScope kubebindv1alpha2.InformerScope + isolation kubebindv1alpha2.Isolation + reconciler reconciler } // NewAPIServiceNamespaceReconciler returns a new APIServiceNamespaceReconciler to reconcile APIServiceNamespaces. @@ -70,10 +70,10 @@ func NewAPIServiceNamespaceReconciler( } r := &APIServiceNamespaceReconciler{ - manager: mgr, - opts: opts, - informerScope: scope, - clusterScopedIsolation: isolation, + manager: mgr, + opts: opts, + informerScope: scope, + isolation: isolation, reconciler: reconciler{ scope: scope, diff --git a/backend/oidc/oidc_test.go b/backend/oidc/oidc_test.go index 8467e8cdd..24775dde2 100644 --- a/backend/oidc/oidc_test.go +++ b/backend/oidc/oidc_test.go @@ -53,6 +53,7 @@ func TestLoadTLSConfig_Success(t *testing.T) { // Verify the TLS config was created if config == nil { t.Fatal("Expected non-nil TLS config") + return } if config.RootCAs == nil { @@ -194,6 +195,7 @@ func TestLoadTLSConfig_MultipleCerts(t *testing.T) { // Verify the TLS config was created if config == nil { t.Fatal("Expected non-nil TLS config") + return } if config.RootCAs == nil { diff --git a/backend/options/options.go b/backend/options/options.go index 53df9c4f4..8bead835a 100644 --- a/backend/options/options.go +++ b/backend/options/options.go @@ -48,14 +48,14 @@ type ExtraOptions struct { Provider string - NamespacePrefix string - PrettyName string - ConsumerScope string - ClusterScopedIsolation string - ExternalAddress string - ExternalCAFile string - ExternalCA []byte - TLSExternalServerName string + NamespacePrefix string + PrettyName string + ConsumerScope string + Isolation string + ExternalAddress string + ExternalCAFile string + ExternalCA []byte + TLSExternalServerName string // Defines the source of the schema for the bind screen. // Options are: // CustomResourceDefinition.v1.apiextensions.k8s.io @@ -101,14 +101,14 @@ func NewOptions() *Options { ProviderKcp: providerkcp.NewOptions(), ExtraOptions: ExtraOptions{ - Provider: "kubernetes", - NamespacePrefix: "cluster-", - PrettyName: "Backend", - ConsumerScope: string(kubebindv1alpha2.NamespacedScope), - ClusterScopedIsolation: string(kubebindv1alpha2.IsolationPrefixed), - SchemaSource: CustomResourceDefinitionSource.String(), - Frontend: "embedded", // Not used, but indicates to use embedded frontend using SPA. - TokenExpiry: 1 * time.Hour, + Provider: "kubernetes", + NamespacePrefix: "cluster-", + PrettyName: "Backend", + ConsumerScope: string(kubebindv1alpha2.NamespacedScope), + Isolation: string(kubebindv1alpha2.IsolationPrefixed), + SchemaSource: CustomResourceDefinitionSource.String(), + Frontend: "embedded", // Not used, but indicates to use embedded frontend using SPA. + TokenExpiry: 1 * time.Hour, }, } return opts @@ -151,7 +151,10 @@ func (options *Options) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&options.NamespacePrefix, "namespace-prefix", options.NamespacePrefix, "The prefix to use for cluster namespaces") fs.StringVar(&options.PrettyName, "pretty-name", options.PrettyName, "Pretty name for the backend") fs.StringVar(&options.ConsumerScope, "consumer-scope", options.ConsumerScope, "How consumers access the service provider cluster. In Kubernetes, \"namespaced\" allows namespace isolation. In kcp, \"cluster\" allows workspace isolation, and with that allows cluster-scoped resources to bind and it is generally more performant.") - fs.StringVar(&options.ClusterScopedIsolation, "cluster-scoped-isolation", options.ClusterScopedIsolation, "How cluster scoped service objects are isolated between multiple consumers on the provider side. Among the choices, \"prefixed\" prepends the name of the cluster namespace to an object's name; \"namespaced\" maps a consumer side object into a namespaced object inside the corresponding cluster namespace; \"none\" is used for the case of a dedicated provider where isolation is not necessary.") + // TODO(mjudeikis): remove deprecated flag in future release + fs.StringVar(&options.Isolation, "cluster-scoped-isolation", options.Isolation, "How cluster scoped service objects are isolated between multiple consumers on the provider side. Among the choices, \"prefixed\" prepends the name of the cluster namespace to an object's name; \"namespaced\" maps a consumer side object into a namespaced object inside the corresponding cluster namespace; \"none\" is used for the case of a dedicated provider where isolation is not necessary.") + _ = fs.MarkDeprecated("cluster-scoped-isolation", "use --isolation instead") + fs.StringVar(&options.Isolation, "isolation", options.Isolation, "Deprecated: use --cluster-scoped-isolation instead. How cluster scoped service objects are isolated between multiple consumers on the provider side. Among the choices, \"prefixed\" prepends the name of the cluster namespace to an object's name; \"namespaced\" maps a consumer side object into a namespaced object inside the corresponding cluster namespace; \"none\" is used for the case of a dedicated provider where isolation is not necessary.") fs.StringVar(&options.ExternalAddress, "external-address", options.ExternalAddress, "The external address for the service provider cluster, including https:// and port. If not specified, service account's hosts are used.") fs.StringVar(&options.ExternalCAFile, "external-ca-file", options.ExternalCAFile, "The external CA file for the service provider cluster. If not specified, service account's CA is used.") fs.StringVar(&options.TLSExternalServerName, "external-server-name", options.TLSExternalServerName, "The external (TLS) server name used by consumers to talk to the service provider cluster. This can be useful to select the right certificate via SNI.") @@ -200,13 +203,13 @@ func (options *Options) Complete() (*CompletedOptions, error) { if strings.ToLower(options.ConsumerScope) == "cluster" { options.ConsumerScope = string(kubebindv1alpha2.ClusterScope) } - switch strings.ToLower(options.ClusterScopedIsolation) { + switch strings.ToLower(options.Isolation) { case "prefixed": - options.ClusterScopedIsolation = string(kubebindv1alpha2.IsolationPrefixed) + options.Isolation = string(kubebindv1alpha2.IsolationPrefixed) case "namespaced": - options.ClusterScopedIsolation = string(kubebindv1alpha2.IsolationNamespaced) + options.Isolation = string(kubebindv1alpha2.IsolationNamespaced) case "none": - options.ClusterScopedIsolation = string(kubebindv1alpha2.IsolationNone) + options.Isolation = string(kubebindv1alpha2.IsolationNone) } if options.ExternalCAFile != "" && options.ExternalCA != nil { diff --git a/backend/server.go b/backend/server.go index e9dcdb874..8960836a8 100644 --- a/backend/server.go +++ b/backend/server.go @@ -179,7 +179,7 @@ func NewServer(ctx context.Context, c *Config) (*Server, error) { s.Config.Manager, opts, kubebindv1alpha2.InformerScope(c.Options.ConsumerScope), - kubebindv1alpha2.Isolation(c.Options.ClusterScopedIsolation), + kubebindv1alpha2.Isolation(c.Options.Isolation), ) if err != nil { return nil, fmt.Errorf("error setting up APIServiceNamespace Controller: %w", err) @@ -195,7 +195,7 @@ func NewServer(ctx context.Context, c *Config) (*Server, error) { s.Config.Manager, opts, kubebindv1alpha2.InformerScope(c.Options.ConsumerScope), - kubebindv1alpha2.Isolation(c.Options.ClusterScopedIsolation), + kubebindv1alpha2.Isolation(c.Options.Isolation), c.Options.SchemaSource, ) if err != nil { diff --git a/contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml b/contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml index c652d179d..3a3e81db7 100644 --- a/contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml +++ b/contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml @@ -62,7 +62,7 @@ spec: crd: {} - group: kube-bind.io name: apiserviceexports - schema: v251112-503d98b.apiserviceexports.kube-bind.io + schema: v251230-e36591a1.apiserviceexports.kube-bind.io storage: crd: {} - group: kube-bind.io diff --git a/contrib/kcp/deploy/resources/apiresourceschema-apiserviceexports.kube-bind.io.yaml b/contrib/kcp/deploy/resources/apiresourceschema-apiserviceexports.kube-bind.io.yaml index 199d1c24b..b22813042 100644 --- a/contrib/kcp/deploy/resources/apiresourceschema-apiserviceexports.kube-bind.io.yaml +++ b/contrib/kcp/deploy/resources/apiresourceschema-apiserviceexports.kube-bind.io.yaml @@ -1,7 +1,7 @@ apiVersion: apis.kcp.io/v1alpha1 kind: APIResourceSchema metadata: - name: v251112-503d98b.apiserviceexports.kube-bind.io + name: v251230-e36591a1.apiserviceexports.kube-bind.io spec: conversion: strategy: None @@ -466,11 +466,15 @@ spec: description: |- ClusterScopedIsolation specifies how cluster scoped service objects are isolated between multiple consumers on the provider side. It can be "Prefixed", "Namespaced", or "None". + Deprecated: use Isolation instead. enum: - Prefixed - Namespaced - None type: string + x-kubernetes-validations: + - message: clusterScopedIsolation is immutable + rule: self == oldSelf informerScope: description: |- informerScope is the scope of the APIServiceExport. It can be either Cluster or Namespace. @@ -486,6 +490,18 @@ spec: x-kubernetes-validations: - message: informerScope is immutable rule: self == oldSelf + isolation: + description: |- + Isolation specifies how service objects are isolated between multiple consumers on the provider side. + It can be "Prefixed", "Namespaced", or "None". + enum: + - Prefixed + - Namespaced + - None + type: string + x-kubernetes-validations: + - message: isolation is immutable + rule: self == oldSelf permissionClaims: description: |- PermissionClaims records decisions about permission claims requested by the service provider. @@ -670,6 +686,7 @@ spec: rule: self == oldSelf required: - informerScope + - isolation - resources type: object status: diff --git a/deploy/charts/backend/crds/kube-bind.io_apiserviceexports.yaml b/deploy/charts/backend/crds/kube-bind.io_apiserviceexports.yaml index bf6ccc3c1..63b3a7b26 100644 --- a/deploy/charts/backend/crds/kube-bind.io_apiserviceexports.yaml +++ b/deploy/charts/backend/crds/kube-bind.io_apiserviceexports.yaml @@ -469,11 +469,15 @@ spec: description: |- ClusterScopedIsolation specifies how cluster scoped service objects are isolated between multiple consumers on the provider side. It can be "Prefixed", "Namespaced", or "None". + Deprecated: use Isolation instead. enum: - Prefixed - Namespaced - None type: string + x-kubernetes-validations: + - message: clusterScopedIsolation is immutable + rule: self == oldSelf informerScope: description: |- informerScope is the scope of the APIServiceExport. It can be either Cluster or Namespace. @@ -489,6 +493,18 @@ spec: x-kubernetes-validations: - message: informerScope is immutable rule: self == oldSelf + isolation: + description: |- + Isolation specifies how service objects are isolated between multiple consumers on the provider side. + It can be "Prefixed", "Namespaced", or "None". + enum: + - Prefixed + - Namespaced + - None + type: string + x-kubernetes-validations: + - message: isolation is immutable + rule: self == oldSelf permissionClaims: description: |- PermissionClaims records decisions about permission claims requested by the service provider. @@ -673,6 +689,7 @@ spec: rule: self == oldSelf required: - informerScope + - isolation - resources type: object status: diff --git a/deploy/charts/backend/templates/deployment.yaml b/deploy/charts/backend/templates/deployment.yaml index 767d1ab11..8085715b7 100644 --- a/deploy/charts/backend/templates/deployment.yaml +++ b/deploy/charts/backend/templates/deployment.yaml @@ -76,8 +76,8 @@ spec: {{- if .Values.backend.consumerScope }} - --consumer-scope={{ .Values.backend.consumerScope }} {{- end }} - {{- if .Values.backend.clusterScopeIsolation }} - - --cluster-scoped-isolation={{ .Values.backend.clusterScopeIsolation }} + {{- if .Values.backend.isolation }} + - --isolation={{ .Values.backend.isolation }} {{- end }} {{- if .Values.backend.cookieSigningKey }} - --cookie-signing-key={{ .Values.backend.cookieSigningKey }} diff --git a/deploy/crd/kube-bind.io_apiserviceexports.yaml b/deploy/crd/kube-bind.io_apiserviceexports.yaml index 4855ff699..5c3ca2f54 100644 --- a/deploy/crd/kube-bind.io_apiserviceexports.yaml +++ b/deploy/crd/kube-bind.io_apiserviceexports.yaml @@ -470,11 +470,15 @@ spec: description: |- ClusterScopedIsolation specifies how cluster scoped service objects are isolated between multiple consumers on the provider side. It can be "Prefixed", "Namespaced", or "None". + Deprecated: use Isolation instead. enum: - Prefixed - Namespaced - None type: string + x-kubernetes-validations: + - message: clusterScopedIsolation is immutable + rule: self == oldSelf informerScope: description: |- informerScope is the scope of the APIServiceExport. It can be either Cluster or Namespace. @@ -490,6 +494,18 @@ spec: x-kubernetes-validations: - message: informerScope is immutable rule: self == oldSelf + isolation: + description: |- + Isolation specifies how service objects are isolated between multiple consumers on the provider side. + It can be "Prefixed", "Namespaced", or "None". + enum: + - Prefixed + - Namespaced + - None + type: string + x-kubernetes-validations: + - message: isolation is immutable + rule: self == oldSelf permissionClaims: description: |- PermissionClaims records decisions about permission claims requested by the service provider. @@ -674,6 +690,7 @@ spec: rule: self == oldSelf required: - informerScope + - isolation - resources type: object status: diff --git a/docs/content/setup/kcp-setup.md b/docs/content/setup/kcp-setup.md index f57975d89..394839f62 100644 --- a/docs/content/setup/kcp-setup.md +++ b/docs/content/setup/kcp-setup.md @@ -50,7 +50,8 @@ go run ./cmd/backend \ --oidc-type=embedded \ --pretty-name="BigCorp.com" \ --namespace-prefix="kube-bind-" \ - --consumer-scope=cluster + --consumer-scope=cluster \ + --isolation=None ``` This process will keep running, so open a new terminal. diff --git a/docs/content/usage/synchronization.md b/docs/content/usage/synchronization.md index ce2547d5c..58da65f1f 100644 --- a/docs/content/usage/synchronization.md +++ b/docs/content/usage/synchronization.md @@ -114,7 +114,7 @@ spec: ### Cluster-Scoped -Cluster-scoped objects require a different approach to isolation than namespaced objects. kube-bind offers three different so-called isolation strategies to deal with them. The strategy to use is configured globally via the `--cluster-scoped-isolation` CLI flag on the kube-bind backend and will from there affect all services offered by that backend. +Cluster-scoped objects require a different approach to isolation than namespaced objects. kube-bind offers three different so-called isolation strategies to deal with them. The strategy to use is configured globally via the `--isolation` CLI flag on the kube-bind backend and will from there affect all services offered by that backend. #### None Strategy @@ -170,4 +170,4 @@ During synchronization, the konnector will then place each cluster-scoped object This strategy provides excellent isolation between consumers, but requires that the original CRD from the provider still makes sense to the consumer when it's suddenly cluster-scoped. For example, if references were to be used, especially to Secrets in the same namespace, this concept would get mangled to some degree during the synchronization. !!! warning - At the moment, when the backend is started with `--cluster-scoped-isolation=namespaced`, it will convert **all namespaced CRDs** in all ServiceExports to become cluster-scoped on the consumer side, even if you intended for a namespaced CRD to stay namespaced on both sides of the sync. + At the moment, when the backend is started with `--isolation=namespaced`, it will convert **all namespaced CRDs** in all ServiceExports to become cluster-scoped on the consumer side, even if you intended for a namespaced CRD to stay namespaced on both sides of the sync. diff --git a/sdk/apis/kubebind/v1alpha2/apiserviceexport_types.go b/sdk/apis/kubebind/v1alpha2/apiserviceexport_types.go index 3ef30971e..2feb07a90 100644 --- a/sdk/apis/kubebind/v1alpha2/apiserviceexport_types.go +++ b/sdk/apis/kubebind/v1alpha2/apiserviceexport_types.go @@ -111,7 +111,17 @@ type APIServiceExportSpec struct { // ClusterScopedIsolation specifies how cluster scoped service objects are isolated between multiple consumers on the provider side. // It can be "Prefixed", "Namespaced", or "None". + // +deprecated + // Deprecated: use Isolation instead. + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="clusterScopedIsolation is immutable" ClusterScopedIsolation Isolation `json:"clusterScopedIsolation,omitempty"` + + // Isolation specifies how service objects are isolated between multiple consumers on the provider side. + // It can be "Prefixed", "Namespaced", or "None". + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="isolation is immutable" + Isolation Isolation `json:"isolation,omitempty"` } // Isolation is an enum defining the different ways to isolate cluster scoped objects diff --git a/test/e2e/bind/happy-case_test.go b/test/e2e/bind/happy-case_test.go index a685a4501..c6d2cfe07 100644 --- a/test/e2e/bind/happy-case_test.go +++ b/test/e2e/bind/happy-case_test.go @@ -298,7 +298,7 @@ func testHappyCase( "--kubeconfig="+providerKubeconfig, "--listen-address=:0", "--consumer-scope="+string(informerScope), - "--cluster-scoped-isolation="+string(isolationStrategy), + "--isolation="+string(isolationStrategy), ) t.Logf("Creating CRD on provider side") From 64df772ffa7108a7ab5ee028c1745aeeb6730710 Mon Sep 17 00:00:00 2001 From: Mangirdas Judeikis Date: Tue, 30 Dec 2025 13:08:15 +0200 Subject: [PATCH 2/5] handle isolation with resources --- .../servicenamespace_controller.go | 3 ++- .../servicenamespace_reconcile.go | 18 ++++++++++++++---- backend/options/options.go | 2 ++ docs/content/usage/api-concepts.md | 2 +- .../serviceexport/serviceexport_reconcile.go | 14 +++++++++----- .../serviceexport/spec/spec_reconcile.go | 1 + 6 files changed, 29 insertions(+), 11 deletions(-) diff --git a/backend/controllers/servicenamespace/servicenamespace_controller.go b/backend/controllers/servicenamespace/servicenamespace_controller.go index 20981ed8f..18a168e49 100644 --- a/backend/controllers/servicenamespace/servicenamespace_controller.go +++ b/backend/controllers/servicenamespace/servicenamespace_controller.go @@ -75,7 +75,8 @@ func NewAPIServiceNamespaceReconciler( informerScope: scope, isolation: isolation, reconciler: reconciler{ - scope: scope, + scope: scope, + isolation: isolation, getNamespace: func(ctx context.Context, cache cache.Cache, name string) (*corev1.Namespace, error) { var ns corev1.Namespace diff --git a/backend/controllers/servicenamespace/servicenamespace_reconcile.go b/backend/controllers/servicenamespace/servicenamespace_reconcile.go index 964537c78..aab88b066 100644 --- a/backend/controllers/servicenamespace/servicenamespace_reconcile.go +++ b/backend/controllers/servicenamespace/servicenamespace_reconcile.go @@ -39,7 +39,8 @@ import ( ) type reconciler struct { - scope kubebindv1alpha2.InformerScope + scope kubebindv1alpha2.InformerScope + isolation kubebindv1alpha2.Isolation getNamespace func(ctx context.Context, cache cache.Cache, name string) (*corev1.Namespace, error) createNamespace func(ctx context.Context, client client.Client, ns *corev1.Namespace) error @@ -52,11 +53,20 @@ type reconciler struct { func (c *reconciler) reconcile(ctx context.Context, client client.Client, cache cache.Cache, sns *kubebindv1alpha2.APIServiceNamespace) error { var ns *corev1.Namespace - nsName := sns.Namespace + "-" + sns.Name - if sns.Status.Namespace != "" { + var nsName string + switch { + case sns.Status.Namespace != "": + // use existing namespace from status nsName = sns.Status.Namespace - ns, _ = c.getNamespace(ctx, cache, nsName) // golint:errcheck + case c.isolation == kubebindv1alpha2.IsolationNone: + nsName = sns.Name + case c.isolation == kubebindv1alpha2.IsolationNamespaced || c.isolation == kubebindv1alpha2.IsolationPrefixed: + nsName = sns.Namespace + "-" + sns.Name + default: + return fmt.Errorf("unknown isolation strategy: %s", c.isolation) } + ns, _ = c.getNamespace(ctx, cache, nsName) + if ns == nil { ns = &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ diff --git a/backend/options/options.go b/backend/options/options.go index 8bead835a..0d4867983 100644 --- a/backend/options/options.go +++ b/backend/options/options.go @@ -210,6 +210,8 @@ func (options *Options) Complete() (*CompletedOptions, error) { options.Isolation = string(kubebindv1alpha2.IsolationNamespaced) case "none": options.Isolation = string(kubebindv1alpha2.IsolationNone) + default: + options.Isolation = string(kubebindv1alpha2.IsolationNone) } if options.ExternalCAFile != "" && options.ExternalCA != nil { diff --git a/docs/content/usage/api-concepts.md b/docs/content/usage/api-concepts.md index 39e4198a6..bd3c5b01b 100644 --- a/docs/content/usage/api-concepts.md +++ b/docs/content/usage/api-concepts.md @@ -114,7 +114,7 @@ spec: consumer: consumer123 # How isolation is done at the provider side - clusterScopedIsolation: Prefixed + isolation: Prefixed # informerScope is the scope of the APIServiceExport. It can be either Cluster or Namespace. # # Cluster: The konnector has permission to watch all namespaces at once and cluster-scoped resources. diff --git a/pkg/konnector/controllers/cluster/serviceexport/serviceexport_reconcile.go b/pkg/konnector/controllers/cluster/serviceexport/serviceexport_reconcile.go index f1ac084cc..8ef02ed2d 100644 --- a/pkg/konnector/controllers/cluster/serviceexport/serviceexport_reconcile.go +++ b/pkg/konnector/controllers/cluster/serviceexport/serviceexport_reconcile.go @@ -144,7 +144,7 @@ func (r *reconciler) ensureControllers(ctx context.Context, namespace, name stri } processedSchemas[name] = true // This is only schemas names (suffix) - isClusterScoped = schema.Spec.Scope == apiextensionsv1.ClusterScoped || schema.Spec.InformerScope == kubebindv1alpha2.ClusterScope + isClusterScoped = schema.Spec.InformerScope == kubebindv1alpha2.ClusterScope } // Ensure controller for permission claims @@ -267,16 +267,20 @@ func (r *reconciler) ensureControllerForSchema(ctx context.Context, export *kube return providerBindClient.KubeBindV1alpha2().APIServiceNamespaces(sn.Namespace).Create(ctx, sn, metav1.CreateOptions{}) }) - case export.Spec.ClusterScopedIsolation == kubebindv1alpha2.IsolationNone: + case export.Spec.Isolation == kubebindv1alpha2.IsolationNone: + logger.V(4).Info("Using None isolation strategy", "export", export.Name) isolationStrategy = isolation.NewNone(r.providerNamespace, providerNamespaceUID) - case export.Spec.ClusterScopedIsolation == kubebindv1alpha2.IsolationPrefixed: + case export.Spec.Isolation == kubebindv1alpha2.IsolationPrefixed: + logger.V(4).Info("Using Prefixed isolation strategy", "export", export.Name) isolationStrategy = isolation.NewPrefixed(r.providerNamespace, providerNamespaceUID) - case export.Spec.ClusterScopedIsolation == kubebindv1alpha2.IsolationNamespaced: + case export.Spec.Isolation == kubebindv1alpha2.IsolationNamespaced: + logger.V(4).Info("Using Namespaced isolation strategy", "export", export.Name) isolationStrategy = isolation.NewNamespaced(r.providerNamespace) default: // Default to None isolation strategy if no valid isolation strategy is specified + logger.V(4).Info("Using default None isolation strategy", "export", export.Name) isolationStrategy = isolation.NewNone(r.providerNamespace, providerNamespaceUID) } @@ -343,7 +347,7 @@ func (r *reconciler) ensureControllersForPermissionClaims( ctx context.Context, export *kubebindv1alpha2.APIServiceExport, binding *kubebindv1alpha2.APIServiceBinding, - isClusterScoped bool, // schema.Spec.Scope == apiextensionsv1.ClusterScoped || schema.Spec.InformerScope == kubebindv1alpha2.ClusterScope + isClusterScoped bool, // schema.Spec.InformerScope == kubebindv1alpha2.ClusterScope ) error { logger := klog.FromContext(ctx) diff --git a/pkg/konnector/controllers/cluster/serviceexport/spec/spec_reconcile.go b/pkg/konnector/controllers/cluster/serviceexport/spec/spec_reconcile.go index 4ee3a250c..e3a2688e2 100644 --- a/pkg/konnector/controllers/cluster/serviceexport/spec/spec_reconcile.go +++ b/pkg/konnector/controllers/cluster/serviceexport/spec/spec_reconcile.go @@ -112,6 +112,7 @@ func (r *reconciler) reconcile(ctx context.Context, obj *unstructured.Unstructur } logger.Info("Creating upstream object") + logger.V(4).Info("Upstream object", "object", fmt.Sprintf("%s", upstream.Object)) if _, err := r.createProviderObject(ctx, upstream); err != nil && !apierrors.IsAlreadyExists(err) { return err } else if apierrors.IsAlreadyExists(err) { From 9c0c58a8886cea7a757d1e8e9be6653e3f030045 Mon Sep 17 00:00:00 2001 From: Mangirdas Judeikis Date: Tue, 30 Dec 2025 13:32:50 +0200 Subject: [PATCH 3/5] move comments to docs --- docs/content/developers/.pages | 1 + docs/content/developers/architecture.md | 315 ++++++++++++++++++++++++ test/e2e/bind/happy-case_test.go | 115 ++------- 3 files changed, 330 insertions(+), 101 deletions(-) create mode 100644 docs/content/developers/architecture.md diff --git a/docs/content/developers/.pages b/docs/content/developers/.pages index ab903794b..bbda7d182 100644 --- a/docs/content/developers/.pages +++ b/docs/content/developers/.pages @@ -1,5 +1,6 @@ nav: - index.md + - Architecture: architecture.md - Development Environment: dev-environments.md - Backend: backend - Konnector: konnector diff --git a/docs/content/developers/architecture.md b/docs/content/developers/architecture.md new file mode 100644 index 000000000..add482ece --- /dev/null +++ b/docs/content/developers/architecture.md @@ -0,0 +1,315 @@ +# Architecture Overview + +This document provides detailed architecture diagrams and explanations for how kube-bind handles resource synchronization between consumer and provider clusters, with a focus on isolation strategies and namespace mapping. + +## Cluster-Scoped Resources + +Cluster-scoped resources (like Sheriffs in our examples) can use different isolation strategies to control how they are placed on the provider cluster and how their associated secrets are isolated. + +### Architecture Flow + +``` +CONSUMER CLUSTER PROVIDER CLUSTER (Backend) +================ ========================== + +┌─────────────────────────────┐ ┌────────────────────────────────────────────┐ +│ Namespace: wild-west │ │ Contract Namespace (per consumer): │ +│ - Secret: sheriff-badge- │──────────▶│ kube-bind- │ +│ credentials (ref) │ Sync │ │ +│ - Secret: sheriff-juris- │ │ APIServiceNamespace CR: │ +│ diction-config (label) │ │ metadata.name: wild-west │ +└─────────────────────────────┘ │ status.namespace: (varies by isolation)│ + └────────────────────────────────────────────┘ +┌─────────────────────────────┐ +│ Sheriff: wyatt-earp │──────────▶ PROVIDER CLUSTER (varies by isolation) +│ (cluster-scoped) │ Sync See isolation strategies below ↓ +│ spec.intent │ +│ spec.secretRefs: [...] │ +└─────────────────────────────┘ + ▲ + │ Status updates + └──────────────────────────────── +``` + +### Isolation Strategies for Cluster-Scoped Resources + +#### 1. IsolationPrefixed + +``` +┌────────────────────────────────────────────┐ +│ [cc-prefixed] IsolationPrefixed: │ +│ Sheriff: -wyatt-earp │ +│ (cluster-scoped, prefixed name) │ +│ Secrets in namespace: │ +│ kube-bind--wild-west │ +│ (APIServiceNamespace mapping) │ +└────────────────────────────────────────────┘ +``` + +**How it works:** +- Sheriff resource name is prefixed with consumer ID: `-wyatt-earp` +- Sheriff remains cluster-scoped on provider +- Secrets are isolated via namespace mapping: `wild-west` → `kube-bind--wild-west` +- Multiple consumers can safely coexist with different prefixed resources + +**Use case:** When you want cluster-scoped resources on the provider but need to support multiple consumers safely. + +#### 2. IsolationNone + +``` +┌────────────────────────────────────────────┐ +│ [cc-none] IsolationNone: │ +│ Sheriff: wyatt-earp │ +│ (cluster-scoped, same name on both!) │ +│ ⚠ Last write wins between consumers │ +│ Secrets in namespace: wild-west │ +│ (Same namespace name on both sides!) │ +└────────────────────────────────────────────┘ +``` + +**How it works:** +- Sheriff resource keeps the same name on both sides: `wyatt-earp` +- Sheriff remains cluster-scoped on provider +- **Namespace mapping is 1:1**: `wild-west` → `wild-west` (same name!) +- **⚠️ WARNING:** No isolation! Multiple consumers share the same namespace. This is possible but discouraged. +- Last write wins if multiple consumers create the same resource + +**Use case:** Single consumer scenarios or when you explicitly want shared resources. **Not recommended for multi-consumer environments.** + +#### 3. IsolationNamespaced + +``` +┌────────────────────────────────────────────┐ +│ [cc-namespaced] IsolationNamespaced: │ +│ Sheriff: wyatt-earp │ +│ (CRD toggled to NamespaceScoped) │ +│ in namespace: │ +│ kube-bind--wild-west │ +│ Secrets in same namespace (isolated) │ +└────────────────────────────────────────────┘ +``` + +**How it works:** +- Sheriff CRD is toggled from ClusterScoped to NamespaceScoped on the provider +- Sheriff is placed in consumer-specific namespace: `kube-bind--wild-west` +- Secrets are in the same namespace (isolated) +- Namespace mapping: `wild-west` → `kube-bind--wild-west` + +**Use case:** When you want the strongest isolation and are willing to modify the CRD scope on the provider. + +### Key Insights for Cluster-Scoped Resources + +The isolation strategy determines **BOTH** how Sheriff resources are placed **AND** how namespaces are mapped: + +- **IsolationNone**: Same namespace name on both sides (`wild-west` → `wild-west`) + - ⚠️ **NO isolation for secrets!** Multiple consumers can exist but share the same namespace (discouraged) + +- **IsolationPrefixed**: Sheriff name prefixed, secrets isolated via namespace mapping + - (`wild-west` → `kube-bind--wild-west`) + +- **IsolationNamespaced**: Sheriff becomes namespaced, secrets isolated in same namespace + - (`wild-west` → `kube-bind--wild-west`) + +## Namespaced Resources + +Namespaced resources (like Cowboys in our examples) always use the ServiceNamespaced isolation strategy, regardless of the isolation parameter. + +### Architecture Flow + +``` +CONSUMER CLUSTER PROVIDER CLUSTER (Backend) +================ ========================== + +┌─────────────────────────────┐ ┌────────────────────────────────────────────┐ +│ Namespace: wild-west │ │ Contract Namespace (per consumer): │ +│ │──────────▶│ kube-bind- │ +│ Cowboy: billy-the-kid │ Sync │ │ +│ (namespaced) │ │ APIServiceNamespace CR: │ +│ spec.intent │ │ metadata.name: wild-west │ +│ spec.secretRefs: [...] │ │ status.namespace: ─────────────────┐ │ +│ │ │ kube-bind--wild-west│ │ +│ - Secret: colt-45-permit │ └────────────────────────────────────────┼───┘ +│ (ref) │ │ +│ - Secret: cowboy-gang- │ ACTUAL PROVIDER NAMESPACE: ◀─────────────┘ +│ affiliation (label) │ ┌────────────────────────────────────────────┐ +└─────────────────────────────┘ │ Namespace: kube-bind--wild- │ + ▲ │ west │ + │ Status updates │ │ + └────────────────────────────────│ Cowboy: billy-the-kid (namespaced) │ + │ - Secret: colt-45-permit │ + │ - Secret: cowboy-gang-affiliation │ + │ │ + │ RBAC (for secret access): │ + │ - Role: kube-binder-export-wild-west- │ + │ cowboys (in this namespace) │ + │ - RoleBinding: ... (in this namespace) │ + └────────────────────────────────────────────┘ +``` + +### InformerScope Variations + +Namespaced resources support two informer scope configurations: + +#### 1. NamespacedScope (nn) + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ [nn] Namespaced → Namespaced (informerScope=NamespacedScope): │ +│ - Consumer: Cowboy in namespace wild-west │ +│ - Provider: Cowboy in namespace kube-bind--wild-west │ +│ - Konnector watches only its own namespace on provider side │ +│ - Natural isolation via namespaces │ +└────────────────────────────────────────────────────────────────────┘ +``` + +**How it works:** +- Consumer resource is namespaced: `wild-west/billy-the-kid` +- Provider resource is namespaced: `kube-bind--wild-west/billy-the-kid` +- Konnector watches **only its own namespace** on the provider side +- RBAC uses namespace-scoped Roles and RoleBindings + +**Use case:** Most efficient for namespaced resources, minimizes RBAC and watch scope. + +#### 2. ClusterScope (nc) + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ [nc] Namespaced → Cluster (informerScope=ClusterScope): │ +│ - Consumer: Cowboy in namespace wild-west │ +│ - Provider: Cowboy in namespace kube-bind--wild-west │ +│ - Konnector watches cluster-wide on provider side │ +│ - Still isolated via namespaces, but different RBAC model │ +└────────────────────────────────────────────────────────────────────┘ +``` + +**How it works:** +- Consumer resource is namespaced: `wild-west/billy-the-kid` +- Provider resource is namespaced: `kube-bind--wild-west/billy-the-kid` +- Konnector watches **cluster-wide** on the provider side +- RBAC uses ClusterRoles and ClusterRoleBindings + +**Use case:** When you need cluster-wide visibility or have specific RBAC requirements. + +### Key Insights for Namespaced Resources + +Namespaced resources **ALWAYS** use the ServiceNamespaced isolation strategy: + +- The `isolation` parameter **only applies to cluster-scoped resources** +- Each consumer gets its own provider namespace via APIServiceNamespace mapping +- Namespace mapping is always: `wild-west` → `kube-bind--wild-west` +- The `informerScope` parameter only affects whether the konnector watches at namespace or cluster level +- Secrets are always isolated per consumer + +## Complete Test Lifecycle + +The end-to-end test validates the complete binding and synchronization flow: + +### Setup Phase + +1. **Create provider workspace (KCP)** + - Install kube-bind CRDs + - Start backend server (HTTP API for binding) + - Bootstrap example CRDs (Cowboys/Sheriffs) + - Apply templates (template-cowboys.yaml / template-sheriffs.yaml) + +2. **Create consumer workspaces** + - Start konnector on each consumer (watches APIServiceBinding) + +### Binding Phase + +3. **Login to provider** + - Simulate browser auth flow + - Save credentials to kube-bind-config.yaml + +4. **List templates & collections** + - Verify backend returns templates + - Verify collections exist + +5. **Bind API** + - Send BindableResourcesRequest with templateRef and clusterIdentity + - Backend creates on provider: + - Contract namespace: `kube-bind-` + - APIServiceExportRequest + - APIServiceNamespace: `wild-west` → `kube-bind--wild-west` + - RBAC resources for secret access + - Backend returns BindingResourceResponse with CRDs, RBAC manifests + +6. **Apply binding on consumer** + - Create APIServiceBinding (watched by konnector) + - Apply CRDs + - Wait for CRD to be established + +### Synchronization & Verification Phase + +7. **Create instance on consumer** + - Create resource (Cowboy or Sheriff) in namespace `wild-west` + - Create associated secrets + +8. **Verify sync to provider** + - Instance created on provider with ClusterNamespaceAnnotation + - Extract providerContractNamespace and providerObjectNamespace + +9. **Verify namespace pre-seeding & RBAC** + - APIServiceNamespace created with status.namespace populated + - Actual provider namespace exists + - RBAC resources created (ClusterRole/Role + Bindings) + +10. **Test bi-directional sync** + - Update spec on consumer → synced to provider + - Update status on provider → synced to consumer + +11. **Verify secret sync** + - Referenced secret (spec.secretRefs) synced to provider namespace + - Label-selected secret synced + +12. **Test deletion** + - Delete on consumer → deleted on provider + +### Multi-Consumer Isolation Verification + +Running with 2 consumers validates: +- Each consumer gets isolated contract namespace +- Each consumer gets isolated provider namespace (except with IsolationNone) +- Secrets are properly isolated (except with IsolationNone) +- For cluster-scoped resources with IsolationNone, last-write-wins +- For cluster-scoped resources with IsolationPrefixed, resources are name-prefixed +- For cluster-scoped resources with IsolationNamespaced, provider CRD is toggled to NamespaceScoped + +## Implementation Details + +### Code Structure + +- **Isolation strategies**: `pkg/konnector/controllers/cluster/serviceexport/isolation/` + - `none.go`: No isolation, same names/namespaces on both sides + - `prefixed.go`: Prefix resource names with consumer ID + - `namespaced.go`: Convert cluster-scoped to namespaced + - `servicenamespaced.go`: APIServiceNamespace-based mapping for namespaced resources + +- **Controller logic**: `pkg/konnector/controllers/cluster/serviceexport/serviceexport_reconcile.go` + - Determines which isolation strategy to use based on resource scope and configuration + - Lines 247-284: Strategy selection logic + +### Strategy Selection Logic + +```go +switch { +case schema.Spec.Scope == apiextensionsv1.NamespaceScoped: + // ALWAYS use ServiceNamespaced for namespaced resources + isolationStrategy = isolation.NewServiceNamespaced(...) + +case export.Spec.Isolation == kubebindv1alpha2.IsolationNone: + isolationStrategy = isolation.NewNone(...) + +case export.Spec.Isolation == kubebindv1alpha2.IsolationPrefixed: + isolationStrategy = isolation.NewPrefixed(...) + +case export.Spec.Isolation == kubebindv1alpha2.IsolationNamespaced: + isolationStrategy = isolation.NewNamespaced(...) +} +``` + +## References + +- Test implementation: `test/e2e/bind/happy-case_test.go` +- API types: `sdk/apis/kubebind/v1alpha2/apiserviceexport_types.go` +- Isolation strategies: `pkg/konnector/controllers/cluster/serviceexport/isolation/` diff --git a/test/e2e/bind/happy-case_test.go b/test/e2e/bind/happy-case_test.go index c6d2cfe07..d9b1f8eb0 100644 --- a/test/e2e/bind/happy-case_test.go +++ b/test/e2e/bind/happy-case_test.go @@ -50,59 +50,13 @@ import ( // TestClusterScoped tests scenarios where consumer-side resources are cluster-scoped (Sheriffs). // Uses: template-sheriffs.yaml (scope=Cluster) & cr-sheriff.yaml // -// ARCHITECTURE FLOW: +// This test validates three isolation strategies for cluster-scoped resources: +// - IsolationPrefixed: Sheriff name is prefixed with consumer ID, secrets isolated +// - IsolationNone: Same names and namespaces on both sides (⚠️ no isolation!) +// - IsolationNamespaced: CRD toggled to NamespaceScoped on provider, secrets isolated // -// CONSUMER CLUSTER PROVIDER CLUSTER (Backend) -// ================ ========================== -// -// ┌─────────────────────────────┐ ┌────────────────────────────────────────────┐ -// │ Namespace: wild-west │ │ Contract Namespace (per consumer): │ -// │ - Secret: sheriff-badge- │──────────▶│ kube-bind- │ -// │ credentials (ref) │ Sync │ │ -// │ - Secret: sheriff-juris- │ │ APIServiceNamespace CR: │ -// │ diction-config (label) │ │ metadata.name: wild-west │ -// └─────────────────────────────┘ │ status.namespace: ─────────────────┐ │ -// │ kube-bind--wild-west│ │ -// ┌─────────────────────────────┐ └────────────────────────────────────────┼───┘ -// │ Sheriff: wyatt-earp │ │ -// │ (cluster-scoped) │──────────▶ ACTUAL PROVIDER NAMESPACE: ◀───────────┘ -// │ spec.intent │ Sync ┌────────────────────────────────────────────┐ -// │ spec.secretRefs: [...] │ │ Namespace: kube-bind--wild- │ -// └─────────────────────────────┘ │ west │ -// ▲ │ - Secret: sheriff-badge-credentials │ -// │ Status updates │ - Secret: sheriff-jurisdiction-config │ -// └────────────────────────────────│ │ -// │ RBAC (for secret access): │ -// │ - ClusterRole: kube-binder-export-wild- │ -// │ west-sheriffs (scope=ClusterScope) │ -// │ - Role: ... (scope=NamespacedScope) │ -// └────────────────────────────────────────────┘ -// -// Sheriff resource location (3 strategies): -// ┌────────────────────────────────────────────┐ -// │ [cc-prefixed] IsolationPrefixed: │ -// │ Sheriff: -wyatt-earp │ -// │ (cluster-scoped, prefixed name) │ -// └────────────────────────────────────────────┘ -// -// ┌────────────────────────────────────────────┐ -// │ [cc-none] IsolationNone: │ -// │ Sheriff: wyatt-earp │ -// │ (cluster-scoped, shared name!) │ -// │ ⚠ Last write wins between consumers │ -// └────────────────────────────────────────────┘ -// -// ┌────────────────────────────────────────────┐ -// │ [cc-namespaced] IsolationNamespaced: │ -// │ Sheriff: wyatt-earp │ -// │ in namespace: kube-bind-- │ -// │ wild-west │ -// │ (CRD toggled to NamespaceScoped) │ -// └────────────────────────────────────────────┘ -// -// KEY INSIGHT: Secrets are ALWAYS isolated per consumer via the contract namespace -// (kube-bind--wild-west), even with IsolationNone. The isolation -// strategy only affects cluster-scoped Sheriff resource naming/placement. +// For detailed architecture diagrams and explanations, see: +// docs/content/developers/architecture.md#cluster-scoped-resources func TestClusterScoped(t *testing.T) { // name & test type defined by letters - cc - cluster-cluster so its easier to identify failures in the logs. testHappyCase(t, "cc-prefixed", apiextensionsv1.ClusterScoped, apiextensionsv1.ClusterScoped, kubebindv1alpha2.ClusterScope, kubebindv1alpha2.IsolationPrefixed) @@ -113,57 +67,16 @@ func TestClusterScoped(t *testing.T) { // TestNamespacedScoped tests scenarios where consumer-side resources are namespaced (Cowboys). // Uses: template-cowboys.yaml (scope=Namespaced) & cr-cowboy.yaml // -// ARCHITECTURE FLOW: -// -// CONSUMER CLUSTER PROVIDER CLUSTER (Backend) -// ================ ========================== -// -// ┌─────────────────────────────┐ ┌────────────────────────────────────────────┐ -// │ Namespace: wild-west │ │ Contract Namespace (per consumer): │ -// │ │──────────▶│ kube-bind- │ -// │ Cowboy: billy-the-kid │ Sync │ │ -// │ (namespaced) │ │ APIServiceNamespace CR: │ -// │ spec.intent │ │ metadata.name: wild-west │ -// │ spec.secretRefs: [...] │ │ status.namespace: ─────────────────┐ │ -// │ │ │ kube-bind--wild-west│ │ -// │ - Secret: colt-45-permit │ └────────────────────────────────────────┼───┘ -// │ (ref) │ │ -// │ - Secret: cowboy-gang- │ ACTUAL PROVIDER NAMESPACE: ◀─────────────┘ -// │ affiliation (label) │ ┌────────────────────────────────────────────┐ -// └─────────────────────────────┘ │ Namespace: kube-bind--wild- │ -// ▲ │ west │ -// │ Status updates │ │ -// └────────────────────────────────│ Cowboy: billy-the-kid (namespaced) │ -// │ - Secret: colt-45-permit │ -// │ - Secret: cowboy-gang-affiliation │ -// │ │ -// │ RBAC (for secret access): │ -// │ - Role: kube-binder-export-wild-west- │ -// │ cowboys (in this namespace) │ -// │ - RoleBinding: ... (in this namespace) │ -// └────────────────────────────────────────────┘ -// -// Two test scenarios: -// ┌────────────────────────────────────────────────────────────────────┐ -// │ [nn] Namespaced → Namespaced (informerScope=NamespacedScope): │ -// │ - Consumer: Cowboy in namespace wild-west │ -// │ - Provider: Cowboy in namespace kube-bind--wild-west │ -// │ - Konnector watches only its own namespace on provider side │ -// │ - Natural isolation via namespaces │ -// └────────────────────────────────────────────────────────────────────┘ +// This test validates two informerScope configurations for namespaced resources: +// - NamespacedScope (nn): Konnector watches only its own namespace on provider +// - ClusterScope (nc): Konnector watches cluster-wide on provider // -// ┌────────────────────────────────────────────────────────────────────┐ -// │ [nc] Namespaced → Cluster (informerScope=ClusterScope): │ -// │ - Consumer: Cowboy in namespace wild-west │ -// │ - Provider: Cowboy in namespace kube-bind--wild-west │ -// │ - Konnector watches cluster-wide on provider side │ -// │ - Still isolated via namespaces, but different RBAC model │ -// └────────────────────────────────────────────────────────────────────┘ +// Note: Namespaced resources ALWAYS use ServiceNamespaced isolation strategy, +// regardless of the isolation parameter. Namespace mapping is always: +// wild-west → kube-bind--wild-west // -// KEY INSIGHT: Namespaced resources are ALWAYS isolated per consumer because -// each consumer gets its own provider namespace (kube-bind--wild-west). -// The informerScope parameter only affects whether the konnector watches at namespace -// or cluster level, not the isolation model. +// For detailed architecture diagrams and explanations, see: +// docs/content/developers/architecture.md#namespaced-resources func TestNamespacedScoped(t *testing.T) { testHappyCase(t, "nn", apiextensionsv1.NamespaceScoped, apiextensionsv1.NamespaceScoped, kubebindv1alpha2.NamespacedScope, "") testHappyCase(t, "nc", apiextensionsv1.NamespaceScoped, apiextensionsv1.NamespaceScoped, kubebindv1alpha2.ClusterScope, "") From 65d1922abbab90c291457e188c3529b3e7eaaa20 Mon Sep 17 00:00:00 2001 From: Mangirdas Judeikis Date: Tue, 30 Dec 2025 15:11:56 +0200 Subject: [PATCH 4/5] Add dynamic dev create resolve --- cli/pkg/kubectl/dev/plugin/create.go | 81 +++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/cli/pkg/kubectl/dev/plugin/create.go b/cli/pkg/kubectl/dev/plugin/create.go index 24709e2c8..ecec4c90b 100644 --- a/cli/pkg/kubectl/dev/plugin/create.go +++ b/cli/pkg/kubectl/dev/plugin/create.go @@ -19,7 +19,10 @@ package plugin import ( "bufio" "context" + "encoding/json" "fmt" + "io" + "net/http" "os" "os/exec" "runtime" @@ -67,9 +70,24 @@ type DevOptions struct { KindNetwork string } +// assetVersion is the version of the kube-bind backend assets used in dev mode +var assetVersion = "" + +// fallbackAssetVersion is used when unable to fetch the latest version +var fallbackAssetVersion = "0.6.0" + +// GitHubRelease represents a GitHub release response +type GitHubRelease struct { + TagName string `json:"tag_name"` +} + // NewDevOptions creates a new DevOptions func NewDevOptions(streams genericclioptions.IOStreams) *DevOptions { opts := base.NewOptions(streams) + // Initialize assetVersion with fallback if not set + if assetVersion == "" { + assetVersion = fallbackAssetVersion + } return &DevOptions{ Options: opts, Logs: logs.NewOptions(), @@ -77,7 +95,7 @@ func NewDevOptions(streams genericclioptions.IOStreams) *DevOptions { ProviderClusterName: "kind-provider", ConsumerClusterName: "kind-consumer", ChartPath: "oci://ghcr.io/kube-bind/charts/backend", - ChartVersion: "v0.6.0", + ChartVersion: assetVersion, } } @@ -92,15 +110,74 @@ func (o *DevOptions) AddCmdFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&o.ChartPath, "chart-path", o.ChartPath, "Helm chart path or OCI registry URL") cmd.Flags().StringVar(&o.ChartVersion, "chart-version", o.ChartVersion, "Helm chart version") cmd.Flags().StringVar(&o.Image, "image", "ghcr.io/kube-bind/backend", "kube-bind backend image to use in dev mode") - cmd.Flags().StringVar(&o.Tag, "tag", "main", "kube-bind backend image tag to use in dev mode") + cmd.Flags().StringVar(&o.Tag, "tag", "v"+assetVersion, "kube-bind backend image tag to use in dev mode") cmd.Flags().StringVar(&o.KindNetwork, "kind-network", "kube-bind-dev", "kind network to use in dev mode") } // Complete completes the options func (o *DevOptions) Complete(args []string) error { + // Only fetch the latest version if assetVersion is not set + if assetVersion == "" { + version, err := fetchLatestRelease() + if err != nil { + // Log the error but continue with fallback version + fmt.Fprintf(o.Streams.ErrOut, "Warning: Failed to fetch latest release version: %v. Using fallback version %s\n", err, fallbackAssetVersion) + assetVersion = fallbackAssetVersion + } else { + assetVersion = version + } + + // Update options with the resolved version + if o.ChartVersion == "" || o.ChartVersion == fallbackAssetVersion { + o.ChartVersion = assetVersion + } + if o.Tag == "" || o.Tag == "v"+fallbackAssetVersion { + o.Tag = "v" + assetVersion + } + } + return nil } +// fetchLatestRelease fetches the latest release version from GitHub +func fetchLatestRelease() (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.github.com/repos/kube-bind/kube-bind/releases/latest", nil) + if err != nil { + return "", fmt.Errorf("failed to create request: %w", err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", fmt.Errorf("failed to fetch latest release: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("GitHub API returned status %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response body: %w", err) + } + + var release GitHubRelease + if err := json.Unmarshal(body, &release); err != nil { + return "", fmt.Errorf("failed to parse release data: %w", err) + } + + if release.TagName == "" { + return "", fmt.Errorf("no tag name in release data") + } + + // Remove 'v' prefix if present + version := strings.TrimPrefix(release.TagName, "v") + return version, nil +} + // Validate validates the options func (o *DevOptions) Validate() error { return o.Options.Validate() From d02df62004cdb4593fba0f5a2c08592b37a941d2 Mon Sep 17 00:00:00 2001 From: Mangirdas Judeikis Date: Mon, 19 Jan 2026 11:32:08 +0200 Subject: [PATCH 5/5] address reviews Signed-off-by: Mangirdas Judeikis --- backend/options/options.go | 2 +- cli/pkg/kubectl/dev/plugin/create.go | 25 +++++++------------ .../resources/apiexport-kube-bind.io.yaml | 2 +- ...schema-apiserviceexports.kube-bind.io.yaml | 10 ++++---- .../crds/kube-bind.io_apiserviceexports.yaml | 8 +++--- .../crd/kube-bind.io_apiserviceexports.yaml | 8 +++--- docs/content/developers/architecture.md | 3 +++ .../serviceexport/serviceexport_reconcile.go | 3 ++- pkg/resources/resources.go | 8 +++--- pkg/resources/resources_test.go | 4 +-- .../v1alpha2/apiserviceexport_types.go | 6 ++--- 11 files changed, 38 insertions(+), 41 deletions(-) diff --git a/backend/options/options.go b/backend/options/options.go index 0d4867983..6a2ab4288 100644 --- a/backend/options/options.go +++ b/backend/options/options.go @@ -154,7 +154,7 @@ func (options *Options) AddFlags(fs *pflag.FlagSet) { // TODO(mjudeikis): remove deprecated flag in future release fs.StringVar(&options.Isolation, "cluster-scoped-isolation", options.Isolation, "How cluster scoped service objects are isolated between multiple consumers on the provider side. Among the choices, \"prefixed\" prepends the name of the cluster namespace to an object's name; \"namespaced\" maps a consumer side object into a namespaced object inside the corresponding cluster namespace; \"none\" is used for the case of a dedicated provider where isolation is not necessary.") _ = fs.MarkDeprecated("cluster-scoped-isolation", "use --isolation instead") - fs.StringVar(&options.Isolation, "isolation", options.Isolation, "Deprecated: use --cluster-scoped-isolation instead. How cluster scoped service objects are isolated between multiple consumers on the provider side. Among the choices, \"prefixed\" prepends the name of the cluster namespace to an object's name; \"namespaced\" maps a consumer side object into a namespaced object inside the corresponding cluster namespace; \"none\" is used for the case of a dedicated provider where isolation is not necessary.") + fs.StringVar(&options.Isolation, "isolation", options.Isolation, "How cluster scoped service objects are isolated between multiple consumers on the provider side. Among the choices, \"prefixed\" prepends the name of the cluster namespace to an object's name; \"namespaced\" maps a consumer side object into a namespaced object inside the corresponding cluster namespace; \"none\" is used for the case of a dedicated provider where isolation is not necessary.") fs.StringVar(&options.ExternalAddress, "external-address", options.ExternalAddress, "The external address for the service provider cluster, including https:// and port. If not specified, service account's hosts are used.") fs.StringVar(&options.ExternalCAFile, "external-ca-file", options.ExternalCAFile, "The external CA file for the service provider cluster. If not specified, service account's CA is used.") fs.StringVar(&options.TLSExternalServerName, "external-server-name", options.TLSExternalServerName, "The external (TLS) server name used by consumers to talk to the service provider cluster. This can be useful to select the right certificate via SNI.") diff --git a/cli/pkg/kubectl/dev/plugin/create.go b/cli/pkg/kubectl/dev/plugin/create.go index ecec4c90b..ebec3a4fb 100644 --- a/cli/pkg/kubectl/dev/plugin/create.go +++ b/cli/pkg/kubectl/dev/plugin/create.go @@ -70,24 +70,17 @@ type DevOptions struct { KindNetwork string } -// assetVersion is the version of the kube-bind backend assets used in dev mode -var assetVersion = "" - // fallbackAssetVersion is used when unable to fetch the latest version -var fallbackAssetVersion = "0.6.0" +const fallbackAssetVersion = "0.6.0" -// GitHubRelease represents a GitHub release response -type GitHubRelease struct { +// gitHubRelease represents a GitHub release response +type gitHubRelease struct { TagName string `json:"tag_name"` } // NewDevOptions creates a new DevOptions func NewDevOptions(streams genericclioptions.IOStreams) *DevOptions { opts := base.NewOptions(streams) - // Initialize assetVersion with fallback if not set - if assetVersion == "" { - assetVersion = fallbackAssetVersion - } return &DevOptions{ Options: opts, Logs: logs.NewOptions(), @@ -95,7 +88,7 @@ func NewDevOptions(streams genericclioptions.IOStreams) *DevOptions { ProviderClusterName: "kind-provider", ConsumerClusterName: "kind-consumer", ChartPath: "oci://ghcr.io/kube-bind/charts/backend", - ChartVersion: assetVersion, + ChartVersion: fallbackAssetVersion, } } @@ -110,14 +103,15 @@ func (o *DevOptions) AddCmdFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&o.ChartPath, "chart-path", o.ChartPath, "Helm chart path or OCI registry URL") cmd.Flags().StringVar(&o.ChartVersion, "chart-version", o.ChartVersion, "Helm chart version") cmd.Flags().StringVar(&o.Image, "image", "ghcr.io/kube-bind/backend", "kube-bind backend image to use in dev mode") - cmd.Flags().StringVar(&o.Tag, "tag", "v"+assetVersion, "kube-bind backend image tag to use in dev mode") + cmd.Flags().StringVar(&o.Tag, "tag", "", "kube-bind backend image tag to use in dev mode") cmd.Flags().StringVar(&o.KindNetwork, "kind-network", "kube-bind-dev", "kind network to use in dev mode") } // Complete completes the options func (o *DevOptions) Complete(args []string) error { - // Only fetch the latest version if assetVersion is not set - if assetVersion == "" { + // Only fetch the latest version if tag is not set + var assetVersion string + if o.Tag == "" { version, err := fetchLatestRelease() if err != nil { // Log the error but continue with fallback version @@ -164,7 +158,7 @@ func fetchLatestRelease() (string, error) { return "", fmt.Errorf("failed to read response body: %w", err) } - var release GitHubRelease + var release gitHubRelease if err := json.Unmarshal(body, &release); err != nil { return "", fmt.Errorf("failed to parse release data: %w", err) } @@ -173,7 +167,6 @@ func fetchLatestRelease() (string, error) { return "", fmt.Errorf("no tag name in release data") } - // Remove 'v' prefix if present version := strings.TrimPrefix(release.TagName, "v") return version, nil } diff --git a/contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml b/contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml index 3a3e81db7..23eb7539d 100644 --- a/contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml +++ b/contrib/kcp/deploy/resources/apiexport-kube-bind.io.yaml @@ -62,7 +62,7 @@ spec: crd: {} - group: kube-bind.io name: apiserviceexports - schema: v251230-e36591a1.apiserviceexports.kube-bind.io + schema: v260119-2e5a3e93.apiserviceexports.kube-bind.io storage: crd: {} - group: kube-bind.io diff --git a/contrib/kcp/deploy/resources/apiresourceschema-apiserviceexports.kube-bind.io.yaml b/contrib/kcp/deploy/resources/apiresourceschema-apiserviceexports.kube-bind.io.yaml index b22813042..758e21b96 100644 --- a/contrib/kcp/deploy/resources/apiresourceschema-apiserviceexports.kube-bind.io.yaml +++ b/contrib/kcp/deploy/resources/apiresourceschema-apiserviceexports.kube-bind.io.yaml @@ -1,7 +1,7 @@ apiVersion: apis.kcp.io/v1alpha1 kind: APIResourceSchema metadata: - name: v251230-e36591a1.apiserviceexports.kube-bind.io + name: v260119-2e5a3e93.apiserviceexports.kube-bind.io spec: conversion: strategy: None @@ -466,7 +466,8 @@ spec: description: |- ClusterScopedIsolation specifies how cluster scoped service objects are isolated between multiple consumers on the provider side. It can be "Prefixed", "Namespaced", or "None". - Deprecated: use Isolation instead. + + Deprecated: ClusterScopedIsolation is deprecated, use Isolation instead. enum: - Prefixed - Namespaced @@ -491,9 +492,8 @@ spec: - message: informerScope is immutable rule: self == oldSelf isolation: - description: |- - Isolation specifies how service objects are isolated between multiple consumers on the provider side. - It can be "Prefixed", "Namespaced", or "None". + description: Isolation specifies how service objects are isolated between + multiple consumers on the provider side. enum: - Prefixed - Namespaced diff --git a/deploy/charts/backend/crds/kube-bind.io_apiserviceexports.yaml b/deploy/charts/backend/crds/kube-bind.io_apiserviceexports.yaml index 63b3a7b26..10f802367 100644 --- a/deploy/charts/backend/crds/kube-bind.io_apiserviceexports.yaml +++ b/deploy/charts/backend/crds/kube-bind.io_apiserviceexports.yaml @@ -469,7 +469,8 @@ spec: description: |- ClusterScopedIsolation specifies how cluster scoped service objects are isolated between multiple consumers on the provider side. It can be "Prefixed", "Namespaced", or "None". - Deprecated: use Isolation instead. + + Deprecated: ClusterScopedIsolation is deprecated, use Isolation instead. enum: - Prefixed - Namespaced @@ -494,9 +495,8 @@ spec: - message: informerScope is immutable rule: self == oldSelf isolation: - description: |- - Isolation specifies how service objects are isolated between multiple consumers on the provider side. - It can be "Prefixed", "Namespaced", or "None". + description: Isolation specifies how service objects are isolated + between multiple consumers on the provider side. enum: - Prefixed - Namespaced diff --git a/deploy/crd/kube-bind.io_apiserviceexports.yaml b/deploy/crd/kube-bind.io_apiserviceexports.yaml index 5c3ca2f54..34f3a3681 100644 --- a/deploy/crd/kube-bind.io_apiserviceexports.yaml +++ b/deploy/crd/kube-bind.io_apiserviceexports.yaml @@ -470,7 +470,8 @@ spec: description: |- ClusterScopedIsolation specifies how cluster scoped service objects are isolated between multiple consumers on the provider side. It can be "Prefixed", "Namespaced", or "None". - Deprecated: use Isolation instead. + + Deprecated: ClusterScopedIsolation is deprecated, use Isolation instead. enum: - Prefixed - Namespaced @@ -495,9 +496,8 @@ spec: - message: informerScope is immutable rule: self == oldSelf isolation: - description: |- - Isolation specifies how service objects are isolated between multiple consumers on the provider side. - It can be "Prefixed", "Namespaced", or "None". + description: Isolation specifies how service objects are isolated + between multiple consumers on the provider side. enum: - Prefixed - Namespaced diff --git a/docs/content/developers/architecture.md b/docs/content/developers/architecture.md index add482ece..550259ea0 100644 --- a/docs/content/developers/architecture.md +++ b/docs/content/developers/architecture.md @@ -2,6 +2,9 @@ This document provides detailed architecture diagrams and explanations for how kube-bind handles resource synchronization between consumer and provider clusters, with a focus on isolation strategies and namespace mapping. +This is development-focused documentation intended to help contributors understand the internal workings of kube-bind. +If you are looking for user-focused documentation, please refer to the [Synchronization User Guide](../usage/synchronization.md). + ## Cluster-Scoped Resources Cluster-scoped resources (like Sheriffs in our examples) can use different isolation strategies to control how they are placed on the provider cluster and how their associated secrets are isolated. diff --git a/pkg/konnector/controllers/cluster/serviceexport/serviceexport_reconcile.go b/pkg/konnector/controllers/cluster/serviceexport/serviceexport_reconcile.go index 8ef02ed2d..3f8ee7ce0 100644 --- a/pkg/konnector/controllers/cluster/serviceexport/serviceexport_reconcile.go +++ b/pkg/konnector/controllers/cluster/serviceexport/serviceexport_reconcile.go @@ -280,7 +280,8 @@ func (r *reconciler) ensureControllerForSchema(ctx context.Context, export *kube isolationStrategy = isolation.NewNamespaced(r.providerNamespace) default: // Default to None isolation strategy if no valid isolation strategy is specified - logger.V(4).Info("Using default None isolation strategy", "export", export.Name) + // This should never happen due to validation, but we add this as a safety net. + logger.V(2).Info("Using default None isolation strategy", "export", export.Name) isolationStrategy = isolation.NewNone(r.providerNamespace, providerNamespaceUID) } diff --git a/pkg/resources/resources.go b/pkg/resources/resources.go index d2eedb729..0a97f35b8 100644 --- a/pkg/resources/resources.go +++ b/pkg/resources/resources.go @@ -208,7 +208,7 @@ func IsClaimedWithReference( if !isReferenceAllowed(ref, apiServiceExport) { logger.Info("reference not allowed, resource is not part of the contract", - "isolation", apiServiceExport.Spec.ClusterScopedIsolation, + "isolation", apiServiceExport.Spec.Isolation, "referenceGroup", ref.Group, "referenceResource", ref.Resource, ) @@ -254,7 +254,7 @@ func IsClaimedWithReference( if consumerSide { logger.Info("checking claim on consumer side") - result := IsClaimed(logger, claim.Selector, copy, potentiallyReferencedResources, apiServiceExport.Spec.ClusterScopedIsolation) + result := IsClaimed(logger, claim.Selector, copy, potentiallyReferencedResources, apiServiceExport.Spec.Isolation) logger.Info("IsClaimed result", "result", result) return result } @@ -280,14 +280,14 @@ func IsClaimedWithReference( copy.SetNamespace(sn.Name) } - result := IsClaimed(logger, claim.Selector, copy, potentiallyReferencedResources, apiServiceExport.Spec.ClusterScopedIsolation) + result := IsClaimed(logger, claim.Selector, copy, potentiallyReferencedResources, apiServiceExport.Spec.Isolation) logger.V(4).Info("IsClaimed result (provider side)", "result", result) return result } func isReferenceAllowed(ref kubebindv1alpha2.SelectorReference, apiServiceExport *kubebindv1alpha2.APIServiceExport) bool { // If isolation is None, everything should be allowed, as both ends are owned. - if apiServiceExport.Spec.ClusterScopedIsolation == kubebindv1alpha2.IsolationNone { + if apiServiceExport.Spec.Isolation == kubebindv1alpha2.IsolationNone { return true } for _, resource := range apiServiceExport.Spec.Resources { diff --git a/pkg/resources/resources_test.go b/pkg/resources/resources_test.go index bf1685dfe..d2f93b2be 100644 --- a/pkg/resources/resources_test.go +++ b/pkg/resources/resources_test.go @@ -1129,8 +1129,8 @@ func TestReferenceSelector_IsolationMode(t *testing.T) { t.Run(tt.name, func(t *testing.T) { export := &kubebindv1alpha2.APIServiceExport{ Spec: kubebindv1alpha2.APIServiceExportSpec{ - ClusterScopedIsolation: tt.isolation, - Resources: tt.exportedResources, + Isolation: tt.isolation, + Resources: tt.exportedResources, }, } got := isReferenceAllowed(tt.reference, export) diff --git a/sdk/apis/kubebind/v1alpha2/apiserviceexport_types.go b/sdk/apis/kubebind/v1alpha2/apiserviceexport_types.go index 2feb07a90..76141bb08 100644 --- a/sdk/apis/kubebind/v1alpha2/apiserviceexport_types.go +++ b/sdk/apis/kubebind/v1alpha2/apiserviceexport_types.go @@ -111,13 +111,13 @@ type APIServiceExportSpec struct { // ClusterScopedIsolation specifies how cluster scoped service objects are isolated between multiple consumers on the provider side. // It can be "Prefixed", "Namespaced", or "None". - // +deprecated - // Deprecated: use Isolation instead. + // + // Deprecated: ClusterScopedIsolation is deprecated, use Isolation instead. + // // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="clusterScopedIsolation is immutable" ClusterScopedIsolation Isolation `json:"clusterScopedIsolation,omitempty"` // Isolation specifies how service objects are isolated between multiple consumers on the provider side. - // It can be "Prefixed", "Namespaced", or "None". // +required // +kubebuilder:validation:Required // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="isolation is immutable"