Support explicit remote namespaces via spec.destination - #2344

Closed
kahirokunn wants to merge 2 commits into
knative:mainfrom
kahirokunn:multicluster-destination
Closed

Support explicit remote namespaces via spec.destination#2344
kahirokunn wants to merge 2 commits into
knative:mainfrom
kahirokunn:multicluster-destination

Conversation

@kahirokunn

@kahirokunnkahirokunn commented Aug 22, 2026

Copy link
Copy Markdown
Member

Fixes#2345

Proposed Changes

  • Replace the top-level spec.clusterProfileRef field with an explicit spec.destination object containing both clusterProfileRef and the remote installation namespace.

Design

This PR makes the destination explicit:

spec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

The complete spec.destination is immutable. Moving an existing installation between clusters or namespaces requires deleting and recreating the management CR so that remote finalization runs before the new destination is installed.

Example CR

KnativeServing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-serving-apacnamespace: management-servingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-servingingress:
gateway-api:
enabled: trueistio:
enabled: false

KnativeEventing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeEventingmetadata:
name: knative-eventing-apacnamespace: management-eventingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-eventing

Local cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-servingnamespace: knative-servingspec: {}# No destination: deploys to the local cluster as before.

Verification Steps

E2E verification with Kind

Verified with three Kind clusters (hub, cluster1, and cluster2), OCM-managed ClusterProfile objects and cluster-proxy endpoints, the managed-serviceaccount ClusterProfileCredSyncer, cp-creds, Envoy Gateway, and cloud-provider-kind.

1. Set up OCM and Cluster Inventory API

The environment follows the OCM ClusterProfile guide: three Kind clusters, both spokes registered with OCM, the sandbox-fleet ManagedClusterSet, cluster-proxy, managed-serviceaccount, and ClusterProfile support.

Enable ClusterProfile credential synchronization in the managed-serviceaccount add-on:

helm upgrade managed-serviceaccount ocm/managed-serviceaccount \
--kube-context kind-hub \
-n open-cluster-management-managed-serviceaccount \
--version 0.10.0 \
--reuse-values \
--set featureGates.clusterProfileCredSyncer=true

Bind the operator namespace to the ManagedClusterSet and label both ManagedServiceAccounts for credential synchronization:

apiVersion: cluster.open-cluster-management.io/v1beta2kind: ManagedClusterSetBindingmetadata:
name: sandbox-fleetnamespace: knative-operatorspec:
clusterSet: sandbox-fleet
kubectl --context kind-hub -n cluster1 label managedserviceaccount knative-operator \
authentication.open-cluster-management.io/sync-to-clusterprofile=true
kubectl --context kind-hub -n cluster2 label managedserviceaccount knative-operator \
authentication.open-cluster-management.io/sync-to-clusterprofile=true

Both ClusterProfiles reported ControlPlaneHealthy=True and Joined=True. The managed-serviceaccount add-on synchronized their credentials into the knative-operator namespace.

2. Configure the operator to use cp-creds

The operator mounted quay.io/open-cluster-management/cp-creds:latest as an image volume at /access-plugins/cp-creds and used this provider configuration:

{
"providers": [
{
"name": "open-cluster-management",
"execConfig": {
"apiVersion": "client.authentication.k8s.io/v1",
"command": "/access-plugins/cp-creds/cp-creds",
"args": ["--managed-serviceaccount=knative-operator"],
"provideClusterInfo": true,
"interactiveMode": "Never"
}
}
]
}

3. Build and deploy this PR head

PATH="$(go env GOPATH)/bin:$PATH" \
KO_DOCKER_REPO=kind.local \
KIND_CLUSTER_NAME=hub \
ko apply -f config/
kubectl --context kind-hub -n knative-operator \
rollout status deployment/knative-operator --timeout=180s

Both destination ClusterProfiles resolved through the configured cp-creds provider.

4. Configure the Serving data plane on cluster1

Gateway API v1.4.1 experimental CRDs and Envoy Gateway v1.7.1 were installed on cluster1 using the same GatewayNamespace deployment model as the original multicluster PR. Both GatewayClasses reported Accepted=True, and both Gateways reported Programmed=True.

5. Deploy Serving and Eventing to explicit remote namespaces

The management CR namespaces intentionally differ from the remote installation namespaces:

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: ocm-servingnamespace: management-serving-hubspec:
destination:
clusterProfileRef:
name: cluster1namespace: knative-operatornamespace: knative-servingnamespace:
labels:
operator.knative.dev/ocm-e2e: cluster1annotations:
operator.knative.dev/ocm-e2e-source: destinationingress:
gateway-api:
enabled: trueistio:
enabled: false
---
apiVersion: operator.knative.dev/v1beta1kind: KnativeEventingmetadata:
name: ocm-eventingnamespace: management-eventing-hubspec:
destination:
clusterProfileRef:
name: cluster2namespace: knative-operatornamespace: knative-eventingnamespace:
labels:
operator.knative.dev/ocm-e2e: cluster2annotations:
operator.knative.dev/ocm-e2e-source: destination

Both hub CRs reached TargetClusterResolved=True and Ready=True. All seven Serving Deployments became Available only in cluster1/knative-serving; Knative Eventing reached Ready=True with its controller, webhook, and broker Deployments only in cluster2/knative-eventing; no Knative Deployments appeared in either hub management namespace or on the wrong spoke. The remote namespace metadata was applied, and every managed Deployment was owned by its remote anchor ConfigMap.

6. Verify Serving traffic

kubectl --context kind-cluster1 apply -f - <<'EOF'apiVersion: serving.knative.dev/v1kind: Servicemetadata: name: helloworld-go namespace: defaultspec: template: spec: containers: - image: gcr.io/knative-samples/helloworld-go env: - name: TARGET value: OCM Native cp-creds E2EEOF
kubectl --context kind-cluster1 -n default \
wait --for=condition=Ready kservice/helloworld-go --timeout=300s
LB_IP=$(kubectl --context kind-cluster1 -n eg-external \ get service knative-external \ -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -H 'Host: helloworld-go.default.example.com'"http://${LB_IP}"

Observed response:

Hello OCM Native cp-creds E2E!

The external and cluster-local HTTPRoutes both reported Accepted=True and ResolvedRefs=True.

7. Verify cleanup with a cold cache

To exercise finalization before the ClusterProfile cache was warm, the operator was restarted and both management CRs were deleted immediately. Both remote anchors and their owned Deployments were removed, and both CRs completed deletion.

After recreating the CRs in the same destination namespaces, both returned to TargetClusterResolved=True and Ready=True.

Breaking change

Existing manifests using the unreleased top-level field must be rewritten:

# Beforespec:
clusterProfileRef:
name: cluster1namespace: cluster-inventory# Afterspec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

Release Note

action required: Replace spec.clusterProfileRef with spec.destination.clusterProfileRef and set spec.destination.namespace for remote Knative installations.

@knative-prowknative-prowBot added the size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. label Aug 22, 2026

@knative-prowknative-prowBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kahirokunn: 0 warnings.

Details

In response to this:

Proposed Changes

  • Replace the top-level spec.clusterProfileRef field with an explicit
    spec.destination object containing both clusterProfileRef and the remote
    installation namespace.
  • Reconcile remote manifests, ingress resources, and the anchor ConfigMap into
    spec.destination.namespace instead of coupling the remote installation to
    the management CR namespace.
  • Make the complete destination immutable and validate the ClusterProfile name,
    ClusterProfile namespace, and installation namespace as Kubernetes names.
  • Update generated CRDs, Helm CRDs, documentation, unit tests, and multicluster
    E2E coverage for the new API shape.

This is an intentional breaking replacement of an unreleased API with no
current consumers. No compatibility or migration path for the removed
top-level spec.clusterProfileRef field is retained.

Design

The existing multicluster reconciliation design remains unchanged: the
operator resolves the referenced Cluster Inventory API ClusterProfile, swaps
the manifest client at the start of reconciliation, and uses an anchor
ConfigMap for namespace-scoped garbage collection on the remote cluster.

This PR makes the destination explicit:

spec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

The management CR namespace and remote installation namespace are independent.
spec.namespace.labels and spec.namespace.annotations are merged into the
selected remote namespace while preserving unrelated existing metadata.

The complete spec.destination is immutable. Moving an existing installation
between clusters or namespaces requires deleting and recreating the management
CR so that remote finalization runs before the new destination is installed.

Example CR

KnativeServing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-serving-apacnamespace: management-servingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-servingingress:
gateway-api:
enabled: trueistio:
enabled: false

KnativeEventing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeEventingmetadata:
name: knative-eventing-apacnamespace: management-eventingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-eventing

Local cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-servingnamespace: knative-servingspec: {}# No destination: deploys to the local cluster as before.

Verification Steps

Automated verification

go test ./...
go test -run '^$' -tags='e2e multicluster' ./test/e2e
./hack/verify-codegen.sh

The repository multicluster Kind suite also passed all 11 tests:

SPOKE_CLUSTER_NAME=codex-destination-spoke \
./test/e2e-tests-multicluster.sh --kubeconfig=<hub-kubeconfig>

E2E verification with Kind

The following end-to-end flow was executed against this PR head. It uses three
Kind clusters (hub, cluster1, and cluster2), real OCM-managed
ClusterProfile objects, the official Cluster Inventory API secretreader
plugin, Envoy Gateway, and cloud-provider-kind.

1. Cluster and OCM setup

The OCM and Cluster Inventory API environment was created following the
OCM ClusterProfile guide:

kind create cluster --name hub
kind create cluster --name cluster1
kind create cluster --name cluster2
# Install OCM on hub, join cluster1 and cluster2, and enable ClusterProfile.# Install sandbox-fleet, cluster-proxy, and managed-serviceaccount.
kubectl --context kind-hub get managedclusters
kubectl --context kind-hub -n cluster-inventory get clusterprofiles
kubectl --context kind-hub get managedclusteraddons -A

Verified state:

  • cluster1 and cluster2: Joined=True, Available=True
  • generated ClusterProfiles: ControlPlaneHealthy=True, Joined=True
  • cluster-proxy and managed-serviceaccount: Available=True

2. Spoke credentials and ClusterProfile access

An OCM ManagedServiceAccount named knative-operator was created for each
managed cluster. The generated ClusterProfiles expose the cluster-proxy access
provider:

kubectl --context kind-hub -n cluster1 get managedserviceaccount knative-operator
kubectl --context kind-hub -n cluster-inventory get clusterprofile cluster1 -o yaml

The operator uses the official secretreader plugin:

{
"providers": [
{
"name": "open-cluster-management",
"execConfig": {
"apiVersion": "client.authentication.k8s.io/v1",
"command": "/access-plugins/secretreader/bin/secretreader-plugin",
"provideClusterInfo": true,
"interactiveMode": "Never"
}
}
]
}

The plugin image was
registry.k8s.io/cluster-inventory-api/secretreader:v0.1.3.

3. Operator deployment

PATH="$(go env GOPATH)/bin:$PATH" \
KO_DOCKER_REPO=kind.local \
KIND_CLUSTER_NAME=hub \
ko apply -f config/
kubectl --context kind-hub -n knative-operator \
rollout status deployment/knative-operator --timeout=180s

The operator was configured with:

--clusterprofile-provider-file=/etc/cluster-inventory/config.json
--remote-deployments-poll-interval=2s

The running operator reported commit 418c364 and successfully resolved the
OCM cluster-proxy endpoint for cluster-inventory/cluster1.

4. Install Envoy Gateway on cluster1

Gateway API v1.4.1 experimental CRDs were installed on the spoke. Envoy Gateway
v1.7.1 was then installed using the same GatewayNamespace deployment model as
the original multicluster PR:

cat <<'EOF' > /tmp/values-eg.yamlconfig: envoyGateway: provider: type: Kubernetes kubernetes: deploy: type: GatewayNamespaceEOF
kubectl --context kind-cluster1 create namespace envoy-gateway-system
helm template eg oci://docker.io/envoyproxy/gateway-helm \
--version v1.7.1 \
-n envoy-gateway-system \
-f /tmp/values-eg.yaml \
--include-crds | \
sed -n '/^---$/,$p'| \
kubectl --context kind-cluster1 apply --server-side --force-conflicts -f -
sudo cloud-provider-kind

5. Create external and internal Gateway resources

Two Envoy Gateway instances were created:

  • eg-external/eg-external: LoadBalancer, ports 80 and 443
  • eg-internal/eg-internal: ClusterIP, port 80
apiVersion: v1kind: Namespacemetadata:
name: eg-external
---
apiVersion: gateway.envoyproxy.io/v1alpha1kind: EnvoyProxymetadata:
name: knative-external-confignamespace: envoy-gateway-systemspec:
provider:
type: Kuberneteskubernetes:
envoyService:
name: knative-external
---
apiVersion: gateway.networking.k8s.io/v1kind: GatewayClassmetadata:
name: eg-externalspec:
controllerName: gateway.envoyproxy.io/gatewayclass-controllerparametersRef:
group: gateway.envoyproxy.iokind: EnvoyProxyname: knative-external-confignamespace: envoy-gateway-system
---
apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata:
name: eg-externalnamespace: eg-externalspec:
gatewayClassName: eg-externallisteners:
- name: httpport: 80protocol: HTTPallowedRoutes:
namespaces:
from: All
- name: tlsport: 443protocol: TLStls:
mode: PassthroughallowedRoutes:
namespaces:
from: All
---
apiVersion: v1kind: Namespacemetadata:
name: eg-internal
---
apiVersion: gateway.envoyproxy.io/v1alpha1kind: EnvoyProxymetadata:
name: knative-internal-confignamespace: envoy-gateway-systemspec:
provider:
type: Kuberneteskubernetes:
envoyService:
type: ClusterIPname: knative-internal
---
apiVersion: gateway.networking.k8s.io/v1kind: GatewayClassmetadata:
name: eg-internalspec:
controllerName: gateway.envoyproxy.io/gatewayclass-controllerparametersRef:
group: gateway.envoyproxy.iokind: EnvoyProxyname: knative-internal-confignamespace: envoy-gateway-system
---
apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata:
name: eg-internalnamespace: eg-internalspec:
gatewayClassName: eg-internallisteners:
- name: httpport: 80protocol: HTTPallowedRoutes:
namespaces:
from: All

Both GatewayClasses reached Accepted=True; both Gateways reached
Programmed=True. The external service received 172.18.0.12 from
cloud-provider-kind.

6. Deploy KnativeServing with an explicit destination

kubectl --context kind-hub apply -f - <<'EOF'apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata: name: ocm-serving namespace: management-serving-hubspec: destination: clusterProfileRef: name: cluster1 namespace: cluster-inventory namespace: knative-serving-ocm ingress: gateway-api: enabled: true istio: enabled: false config: config-gateway: external-gateways: | - class: eg-external gateway: eg-external/eg-external service: eg-external/knative-external supported-features: - HTTPRouteRequestTimeout local-gateways: | - class: eg-internal gateway: eg-internal/eg-internal service: eg-internal/knative-internal supported-features: - HTTPRouteRequestTimeout network: ingress-class: gateway-api.ingress.networking.knative.dev domain: example.com: ""EOF

The hub CR reached Ready=True and TargetClusterResolved=True. All seven
Knative Serving Deployments became Available in
cluster1/knative-serving-ocm, with no Serving Deployment in the hub management
namespace or on cluster2.

7. Verify ownership and data-plane traffic

The remote anchor was present and protected:

kubectl --context kind-cluster1 -n knative-serving-ocm \
get configmap knativeserving-ocm-serving-root-owner -o yaml
kubectl --context kind-cluster1 -n knative-serving-ocm \
get deployment activator -o jsonpath='{.metadata.ownerReferences}'| jq .

The activator Deployment ownerReference matched the anchor ConfigMap UID.

A Knative Service was then deployed on cluster1:

kubectl --context kind-cluster1 apply -f - <<'EOF'apiVersion: serving.knative.dev/v1kind: Servicemetadata: name: helloworld-go namespace: defaultspec: template: spec: containers: - image: gcr.io/knative-samples/helloworld-go env: - name: TARGET value: Destination Kind E2EEOF
kubectl --context kind-cluster1 -n default \
wait --for=condition=Ready kservice/helloworld-go --timeout=300s
LB_IP=$(kubectl --context kind-cluster1 -n eg-external \ get service knative-external \ -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -H 'Host: helloworld-go.default.example.com'"http://${LB_IP}"

Observed response:

Hello Destination Kind E2E!

The external and cluster-local HTTPRoutes both reported Accepted=True.

8. Verify CR deletion and finalizer cleanup

kubectl --context kind-cluster1 -n default delete kservice helloworld-go
kubectl --context kind-hub -n management-serving-hub \
delete knativeserving ocm-serving

Verified after finalization:

  • the hub KnativeServing CR was deleted
  • the remote anchor ConfigMap was deleted
  • all labeled Knative Serving Deployments were deleted
  • all Knative Serving ClusterRoles were deleted

The dynamically created autoscaler-bucket-00-of-01 Service and leader-election
Leases do not carry the anchor ownerReference and can remain after uninstall.
This also occurs on fork/main and is not introduced by the destination API
replacement. Those test-only remnants were removed before reinstalling into the
same namespace.

9. Recreate and leave the environment healthy

The same KnativeServing CR was recreated after cleanup. It returned to
Ready=True and TargetClusterResolved=True, and all seven Serving Deployments
became Available again. The OCM hub, both managed clusters, Envoy Gateway,
Knative Serving, and Knative Eventing were left running for inspection.

Destination validation

The real API server rejected all of the following:

  • invalid destination namespace
  • missing ClusterProfile name, ClusterProfile namespace, or target namespace
  • legacy top-level spec.clusterProfileRef
  • changes to destination.namespace
  • changes to destination.clusterProfileRef.name
  • changes to destination.clusterProfileRef.namespace
  • removal of spec.destination

Intentional breaking change

Existing manifests using the unreleased top-level field must be rewritten:

# Beforespec:
clusterProfileRef:
name: cluster1namespace: cluster-inventory# Afterspec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

No automatic migration or compatibility field is provided because the old API
has no current consumers.

Release Note

action required: Replace spec.clusterProfileRef with spec.destination.clusterProfileRef and set spec.destination.namespace for remote Knative installations.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@knative-prow

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: kahirokunn
Once this PR has been reviewed and has the lgtm label, please assign dsimansk for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@codecov

codecovBot commented Aug 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.14286% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.54%. Comparing base (0fc378e) to head (a5ca744).
⚠️ Report is 2 commits behind head on main.

Files with missing linesPatch %Lines
pkg/apis/operator/base/common.go0.00%2 Missing ⚠️
pkg/reconciler/common/multicluster.go94.73%1 Missing ⚠️
pkg/reconciler/knativeeventing/controller.go0.00%1 Missing ⚠️
pkg/reconciler/knativeserving/controller.go0.00%1 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #2344 +/- ##
==========================================
+ Coverage 63.99% 64.54% +0.55% 
==========================================
Files 55 55 Lines 2491 2502 +11 ==========================================
+ Hits 1594 1615 +21 + Misses 777 761 -16 - Partials 120 126 +6 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@kahirokunn

Copy link
Copy Markdown
MemberAuthor

/retest

@kahirokunn
kahirokunn marked this pull request as draft August 23, 2026 00:47
@knative-prowknative-prowBot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 23, 2026
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 418c364 to 449028eCompareAugust 23, 2026 00:54
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 449028e to 7345ad9CompareAugust 23, 2026 01:11
Signed-off-by: kahirokunn <okinakahiro@gmail.com>
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 7345ad9 to 8447d27CompareAugust 23, 2026 13:24
@kahirokunn
kahirokunn marked this pull request as ready for review August 23, 2026 13:28
@knative-prowknative-prowBot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 23, 2026
@knative-prow
knative-prowBot requested a review from matzewAugust 23, 2026 13:28
Signed-off-by: kahirokunn <okinakahiro@gmail.com>
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 8447d27 to a5ca744CompareAugust 23, 2026 13:29
@dsimansk

Copy link
Copy Markdown
Contributor

@kahirokunn is running multiple CRs of the same kind in parallel namespaces supported use case now? I.e. KnativeServing being on namespace: ns1, namespace: ns2 based on the same cluster profile?

@kahirokunn

Copy link
Copy Markdown
MemberAuthor

No, multiple CRs of the same kind targeting the same cluster are not intended to be supported.
I recalled the earlier discussion that each cluster supports only one Serving and one Eventing installation.
#1472 (comment)

Therefore, allowing an arbitrary destination namespace may be the wrong API design. The management CR may live in any hub namespace, while the remote installation namespace should probably be derived from the kind (knative-serving or knative-eventing). We should also prevent multiple CRs of the same kind from targeting the same ClusterProfile.

@kahirokunn
kahirokunn marked this pull request as draft August 26, 2026 02:16
@knative-prowknative-prowBot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 26, 2026
@kahirokunn

Copy link
Copy Markdown
MemberAuthor

Move to #2349

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge/work-in-progressIndicates that a PR should not merge because it is a work in progress.size/XLDenotes a PR that changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Install remote Knative components in canonical namespaces

2 participants

@kahirokunn@dsimansk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Support explicit remote namespaces via spec.destination - #2344

Closed
kahirokunn wants to merge 2 commits into
knative:mainfrom
kahirokunn:multicluster-destination
Closed

Support explicit remote namespaces via spec.destination#2344
kahirokunn wants to merge 2 commits into
knative:mainfrom
kahirokunn:multicluster-destination

Conversation

@kahirokunn

@kahirokunnkahirokunn commented Aug 22, 2026

Copy link
Copy Markdown
Member

Fixes#2345

Proposed Changes

  • Replace the top-level spec.clusterProfileRef field with an explicit spec.destination object containing both clusterProfileRef and the remote installation namespace.

Design

This PR makes the destination explicit:

spec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

The complete spec.destination is immutable. Moving an existing installation between clusters or namespaces requires deleting and recreating the management CR so that remote finalization runs before the new destination is installed.

Example CR

KnativeServing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-serving-apacnamespace: management-servingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-servingingress:
gateway-api:
enabled: trueistio:
enabled: false

KnativeEventing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeEventingmetadata:
name: knative-eventing-apacnamespace: management-eventingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-eventing

Local cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-servingnamespace: knative-servingspec: {}# No destination: deploys to the local cluster as before.

Verification Steps

E2E verification with Kind

Verified with three Kind clusters (hub, cluster1, and cluster2), OCM-managed ClusterProfile objects and cluster-proxy endpoints, the managed-serviceaccount ClusterProfileCredSyncer, cp-creds, Envoy Gateway, and cloud-provider-kind.

1. Set up OCM and Cluster Inventory API

The environment follows the OCM ClusterProfile guide: three Kind clusters, both spokes registered with OCM, the sandbox-fleet ManagedClusterSet, cluster-proxy, managed-serviceaccount, and ClusterProfile support.

Enable ClusterProfile credential synchronization in the managed-serviceaccount add-on:

helm upgrade managed-serviceaccount ocm/managed-serviceaccount \
--kube-context kind-hub \
-n open-cluster-management-managed-serviceaccount \
--version 0.10.0 \
--reuse-values \
--set featureGates.clusterProfileCredSyncer=true

Bind the operator namespace to the ManagedClusterSet and label both ManagedServiceAccounts for credential synchronization:

apiVersion: cluster.open-cluster-management.io/v1beta2kind: ManagedClusterSetBindingmetadata:
name: sandbox-fleetnamespace: knative-operatorspec:
clusterSet: sandbox-fleet
kubectl --context kind-hub -n cluster1 label managedserviceaccount knative-operator \
authentication.open-cluster-management.io/sync-to-clusterprofile=true
kubectl --context kind-hub -n cluster2 label managedserviceaccount knative-operator \
authentication.open-cluster-management.io/sync-to-clusterprofile=true

Both ClusterProfiles reported ControlPlaneHealthy=True and Joined=True. The managed-serviceaccount add-on synchronized their credentials into the knative-operator namespace.

2. Configure the operator to use cp-creds

The operator mounted quay.io/open-cluster-management/cp-creds:latest as an image volume at /access-plugins/cp-creds and used this provider configuration:

{
"providers": [
{
"name": "open-cluster-management",
"execConfig": {
"apiVersion": "client.authentication.k8s.io/v1",
"command": "/access-plugins/cp-creds/cp-creds",
"args": ["--managed-serviceaccount=knative-operator"],
"provideClusterInfo": true,
"interactiveMode": "Never"
}
}
]
}

3. Build and deploy this PR head

PATH="$(go env GOPATH)/bin:$PATH" \
KO_DOCKER_REPO=kind.local \
KIND_CLUSTER_NAME=hub \
ko apply -f config/
kubectl --context kind-hub -n knative-operator \
rollout status deployment/knative-operator --timeout=180s

Both destination ClusterProfiles resolved through the configured cp-creds provider.

4. Configure the Serving data plane on cluster1

Gateway API v1.4.1 experimental CRDs and Envoy Gateway v1.7.1 were installed on cluster1 using the same GatewayNamespace deployment model as the original multicluster PR. Both GatewayClasses reported Accepted=True, and both Gateways reported Programmed=True.

5. Deploy Serving and Eventing to explicit remote namespaces

The management CR namespaces intentionally differ from the remote installation namespaces:

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: ocm-servingnamespace: management-serving-hubspec:
destination:
clusterProfileRef:
name: cluster1namespace: knative-operatornamespace: knative-servingnamespace:
labels:
operator.knative.dev/ocm-e2e: cluster1annotations:
operator.knative.dev/ocm-e2e-source: destinationingress:
gateway-api:
enabled: trueistio:
enabled: false
---
apiVersion: operator.knative.dev/v1beta1kind: KnativeEventingmetadata:
name: ocm-eventingnamespace: management-eventing-hubspec:
destination:
clusterProfileRef:
name: cluster2namespace: knative-operatornamespace: knative-eventingnamespace:
labels:
operator.knative.dev/ocm-e2e: cluster2annotations:
operator.knative.dev/ocm-e2e-source: destination

Both hub CRs reached TargetClusterResolved=True and Ready=True. All seven Serving Deployments became Available only in cluster1/knative-serving; Knative Eventing reached Ready=True with its controller, webhook, and broker Deployments only in cluster2/knative-eventing; no Knative Deployments appeared in either hub management namespace or on the wrong spoke. The remote namespace metadata was applied, and every managed Deployment was owned by its remote anchor ConfigMap.

6. Verify Serving traffic

kubectl --context kind-cluster1 apply -f - <<'EOF'apiVersion: serving.knative.dev/v1kind: Servicemetadata: name: helloworld-go namespace: defaultspec: template: spec: containers: - image: gcr.io/knative-samples/helloworld-go env: - name: TARGET value: OCM Native cp-creds E2EEOF
kubectl --context kind-cluster1 -n default \
wait --for=condition=Ready kservice/helloworld-go --timeout=300s
LB_IP=$(kubectl --context kind-cluster1 -n eg-external \ get service knative-external \ -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -H 'Host: helloworld-go.default.example.com'"http://${LB_IP}"

Observed response:

Hello OCM Native cp-creds E2E!

The external and cluster-local HTTPRoutes both reported Accepted=True and ResolvedRefs=True.

7. Verify cleanup with a cold cache

To exercise finalization before the ClusterProfile cache was warm, the operator was restarted and both management CRs were deleted immediately. Both remote anchors and their owned Deployments were removed, and both CRs completed deletion.

After recreating the CRs in the same destination namespaces, both returned to TargetClusterResolved=True and Ready=True.

Breaking change

Existing manifests using the unreleased top-level field must be rewritten:

# Beforespec:
clusterProfileRef:
name: cluster1namespace: cluster-inventory# Afterspec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

Release Note

action required: Replace spec.clusterProfileRef with spec.destination.clusterProfileRef and set spec.destination.namespace for remote Knative installations.

@knative-prowknative-prowBot added the size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. label Aug 22, 2026

@knative-prowknative-prowBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kahirokunn: 0 warnings.

Details

In response to this:

Proposed Changes

  • Replace the top-level spec.clusterProfileRef field with an explicit
    spec.destination object containing both clusterProfileRef and the remote
    installation namespace.
  • Reconcile remote manifests, ingress resources, and the anchor ConfigMap into
    spec.destination.namespace instead of coupling the remote installation to
    the management CR namespace.
  • Make the complete destination immutable and validate the ClusterProfile name,
    ClusterProfile namespace, and installation namespace as Kubernetes names.
  • Update generated CRDs, Helm CRDs, documentation, unit tests, and multicluster
    E2E coverage for the new API shape.

This is an intentional breaking replacement of an unreleased API with no
current consumers. No compatibility or migration path for the removed
top-level spec.clusterProfileRef field is retained.

Design

The existing multicluster reconciliation design remains unchanged: the
operator resolves the referenced Cluster Inventory API ClusterProfile, swaps
the manifest client at the start of reconciliation, and uses an anchor
ConfigMap for namespace-scoped garbage collection on the remote cluster.

This PR makes the destination explicit:

spec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

The management CR namespace and remote installation namespace are independent.
spec.namespace.labels and spec.namespace.annotations are merged into the
selected remote namespace while preserving unrelated existing metadata.

The complete spec.destination is immutable. Moving an existing installation
between clusters or namespaces requires deleting and recreating the management
CR so that remote finalization runs before the new destination is installed.

Example CR

KnativeServing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-serving-apacnamespace: management-servingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-servingingress:
gateway-api:
enabled: trueistio:
enabled: false

KnativeEventing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeEventingmetadata:
name: knative-eventing-apacnamespace: management-eventingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-eventing

Local cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-servingnamespace: knative-servingspec: {}# No destination: deploys to the local cluster as before.

Verification Steps

Automated verification

go test ./...
go test -run '^$' -tags='e2e multicluster' ./test/e2e
./hack/verify-codegen.sh

The repository multicluster Kind suite also passed all 11 tests:

SPOKE_CLUSTER_NAME=codex-destination-spoke \
./test/e2e-tests-multicluster.sh --kubeconfig=<hub-kubeconfig>

E2E verification with Kind

The following end-to-end flow was executed against this PR head. It uses three
Kind clusters (hub, cluster1, and cluster2), real OCM-managed
ClusterProfile objects, the official Cluster Inventory API secretreader
plugin, Envoy Gateway, and cloud-provider-kind.

1. Cluster and OCM setup

The OCM and Cluster Inventory API environment was created following the
OCM ClusterProfile guide:

kind create cluster --name hub
kind create cluster --name cluster1
kind create cluster --name cluster2
# Install OCM on hub, join cluster1 and cluster2, and enable ClusterProfile.# Install sandbox-fleet, cluster-proxy, and managed-serviceaccount.
kubectl --context kind-hub get managedclusters
kubectl --context kind-hub -n cluster-inventory get clusterprofiles
kubectl --context kind-hub get managedclusteraddons -A

Verified state:

  • cluster1 and cluster2: Joined=True, Available=True
  • generated ClusterProfiles: ControlPlaneHealthy=True, Joined=True
  • cluster-proxy and managed-serviceaccount: Available=True

2. Spoke credentials and ClusterProfile access

An OCM ManagedServiceAccount named knative-operator was created for each
managed cluster. The generated ClusterProfiles expose the cluster-proxy access
provider:

kubectl --context kind-hub -n cluster1 get managedserviceaccount knative-operator
kubectl --context kind-hub -n cluster-inventory get clusterprofile cluster1 -o yaml

The operator uses the official secretreader plugin:

{
"providers": [
{
"name": "open-cluster-management",
"execConfig": {
"apiVersion": "client.authentication.k8s.io/v1",
"command": "/access-plugins/secretreader/bin/secretreader-plugin",
"provideClusterInfo": true,
"interactiveMode": "Never"
}
}
]
}

The plugin image was
registry.k8s.io/cluster-inventory-api/secretreader:v0.1.3.

3. Operator deployment

PATH="$(go env GOPATH)/bin:$PATH" \
KO_DOCKER_REPO=kind.local \
KIND_CLUSTER_NAME=hub \
ko apply -f config/
kubectl --context kind-hub -n knative-operator \
rollout status deployment/knative-operator --timeout=180s

The operator was configured with:

--clusterprofile-provider-file=/etc/cluster-inventory/config.json
--remote-deployments-poll-interval=2s

The running operator reported commit 418c364 and successfully resolved the
OCM cluster-proxy endpoint for cluster-inventory/cluster1.

4. Install Envoy Gateway on cluster1

Gateway API v1.4.1 experimental CRDs were installed on the spoke. Envoy Gateway
v1.7.1 was then installed using the same GatewayNamespace deployment model as
the original multicluster PR:

cat <<'EOF' > /tmp/values-eg.yamlconfig: envoyGateway: provider: type: Kubernetes kubernetes: deploy: type: GatewayNamespaceEOF
kubectl --context kind-cluster1 create namespace envoy-gateway-system
helm template eg oci://docker.io/envoyproxy/gateway-helm \
--version v1.7.1 \
-n envoy-gateway-system \
-f /tmp/values-eg.yaml \
--include-crds | \
sed -n '/^---$/,$p'| \
kubectl --context kind-cluster1 apply --server-side --force-conflicts -f -
sudo cloud-provider-kind

5. Create external and internal Gateway resources

Two Envoy Gateway instances were created:

  • eg-external/eg-external: LoadBalancer, ports 80 and 443
  • eg-internal/eg-internal: ClusterIP, port 80
apiVersion: v1kind: Namespacemetadata:
name: eg-external
---
apiVersion: gateway.envoyproxy.io/v1alpha1kind: EnvoyProxymetadata:
name: knative-external-confignamespace: envoy-gateway-systemspec:
provider:
type: Kuberneteskubernetes:
envoyService:
name: knative-external
---
apiVersion: gateway.networking.k8s.io/v1kind: GatewayClassmetadata:
name: eg-externalspec:
controllerName: gateway.envoyproxy.io/gatewayclass-controllerparametersRef:
group: gateway.envoyproxy.iokind: EnvoyProxyname: knative-external-confignamespace: envoy-gateway-system
---
apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata:
name: eg-externalnamespace: eg-externalspec:
gatewayClassName: eg-externallisteners:
- name: httpport: 80protocol: HTTPallowedRoutes:
namespaces:
from: All
- name: tlsport: 443protocol: TLStls:
mode: PassthroughallowedRoutes:
namespaces:
from: All
---
apiVersion: v1kind: Namespacemetadata:
name: eg-internal
---
apiVersion: gateway.envoyproxy.io/v1alpha1kind: EnvoyProxymetadata:
name: knative-internal-confignamespace: envoy-gateway-systemspec:
provider:
type: Kuberneteskubernetes:
envoyService:
type: ClusterIPname: knative-internal
---
apiVersion: gateway.networking.k8s.io/v1kind: GatewayClassmetadata:
name: eg-internalspec:
controllerName: gateway.envoyproxy.io/gatewayclass-controllerparametersRef:
group: gateway.envoyproxy.iokind: EnvoyProxyname: knative-internal-confignamespace: envoy-gateway-system
---
apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata:
name: eg-internalnamespace: eg-internalspec:
gatewayClassName: eg-internallisteners:
- name: httpport: 80protocol: HTTPallowedRoutes:
namespaces:
from: All

Both GatewayClasses reached Accepted=True; both Gateways reached
Programmed=True. The external service received 172.18.0.12 from
cloud-provider-kind.

6. Deploy KnativeServing with an explicit destination

kubectl --context kind-hub apply -f - <<'EOF'apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata: name: ocm-serving namespace: management-serving-hubspec: destination: clusterProfileRef: name: cluster1 namespace: cluster-inventory namespace: knative-serving-ocm ingress: gateway-api: enabled: true istio: enabled: false config: config-gateway: external-gateways: | - class: eg-external gateway: eg-external/eg-external service: eg-external/knative-external supported-features: - HTTPRouteRequestTimeout local-gateways: | - class: eg-internal gateway: eg-internal/eg-internal service: eg-internal/knative-internal supported-features: - HTTPRouteRequestTimeout network: ingress-class: gateway-api.ingress.networking.knative.dev domain: example.com: ""EOF

The hub CR reached Ready=True and TargetClusterResolved=True. All seven
Knative Serving Deployments became Available in
cluster1/knative-serving-ocm, with no Serving Deployment in the hub management
namespace or on cluster2.

7. Verify ownership and data-plane traffic

The remote anchor was present and protected:

kubectl --context kind-cluster1 -n knative-serving-ocm \
get configmap knativeserving-ocm-serving-root-owner -o yaml
kubectl --context kind-cluster1 -n knative-serving-ocm \
get deployment activator -o jsonpath='{.metadata.ownerReferences}'| jq .

The activator Deployment ownerReference matched the anchor ConfigMap UID.

A Knative Service was then deployed on cluster1:

kubectl --context kind-cluster1 apply -f - <<'EOF'apiVersion: serving.knative.dev/v1kind: Servicemetadata: name: helloworld-go namespace: defaultspec: template: spec: containers: - image: gcr.io/knative-samples/helloworld-go env: - name: TARGET value: Destination Kind E2EEOF
kubectl --context kind-cluster1 -n default \
wait --for=condition=Ready kservice/helloworld-go --timeout=300s
LB_IP=$(kubectl --context kind-cluster1 -n eg-external \ get service knative-external \ -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -H 'Host: helloworld-go.default.example.com'"http://${LB_IP}"

Observed response:

Hello Destination Kind E2E!

The external and cluster-local HTTPRoutes both reported Accepted=True.

8. Verify CR deletion and finalizer cleanup

kubectl --context kind-cluster1 -n default delete kservice helloworld-go
kubectl --context kind-hub -n management-serving-hub \
delete knativeserving ocm-serving

Verified after finalization:

  • the hub KnativeServing CR was deleted
  • the remote anchor ConfigMap was deleted
  • all labeled Knative Serving Deployments were deleted
  • all Knative Serving ClusterRoles were deleted

The dynamically created autoscaler-bucket-00-of-01 Service and leader-election
Leases do not carry the anchor ownerReference and can remain after uninstall.
This also occurs on fork/main and is not introduced by the destination API
replacement. Those test-only remnants were removed before reinstalling into the
same namespace.

9. Recreate and leave the environment healthy

The same KnativeServing CR was recreated after cleanup. It returned to
Ready=True and TargetClusterResolved=True, and all seven Serving Deployments
became Available again. The OCM hub, both managed clusters, Envoy Gateway,
Knative Serving, and Knative Eventing were left running for inspection.

Destination validation

The real API server rejected all of the following:

  • invalid destination namespace
  • missing ClusterProfile name, ClusterProfile namespace, or target namespace
  • legacy top-level spec.clusterProfileRef
  • changes to destination.namespace
  • changes to destination.clusterProfileRef.name
  • changes to destination.clusterProfileRef.namespace
  • removal of spec.destination

Intentional breaking change

Existing manifests using the unreleased top-level field must be rewritten:

# Beforespec:
clusterProfileRef:
name: cluster1namespace: cluster-inventory# Afterspec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

No automatic migration or compatibility field is provided because the old API
has no current consumers.

Release Note

action required: Replace spec.clusterProfileRef with spec.destination.clusterProfileRef and set spec.destination.namespace for remote Knative installations.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@knative-prow

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: kahirokunn
Once this PR has been reviewed and has the lgtm label, please assign dsimansk for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@codecov

codecovBot commented Aug 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.14286% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.54%. Comparing base (0fc378e) to head (a5ca744).
⚠️ Report is 2 commits behind head on main.

Files with missing linesPatch %Lines
pkg/apis/operator/base/common.go0.00%2 Missing ⚠️
pkg/reconciler/common/multicluster.go94.73%1 Missing ⚠️
pkg/reconciler/knativeeventing/controller.go0.00%1 Missing ⚠️
pkg/reconciler/knativeserving/controller.go0.00%1 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #2344 +/- ##
==========================================
+ Coverage 63.99% 64.54% +0.55% 
==========================================
Files 55 55 Lines 2491 2502 +11 ==========================================
+ Hits 1594 1615 +21 + Misses 777 761 -16 - Partials 120 126 +6 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@kahirokunn

Copy link
Copy Markdown
MemberAuthor

/retest

@kahirokunn
kahirokunn marked this pull request as draft August 23, 2026 00:47
@knative-prowknative-prowBot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 23, 2026
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 418c364 to 449028eCompareAugust 23, 2026 00:54
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 449028e to 7345ad9CompareAugust 23, 2026 01:11
Signed-off-by: kahirokunn <okinakahiro@gmail.com>
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 7345ad9 to 8447d27CompareAugust 23, 2026 13:24
@kahirokunn
kahirokunn marked this pull request as ready for review August 23, 2026 13:28
@knative-prowknative-prowBot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 23, 2026
@knative-prow
knative-prowBot requested a review from matzewAugust 23, 2026 13:28
Signed-off-by: kahirokunn <okinakahiro@gmail.com>
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 8447d27 to a5ca744CompareAugust 23, 2026 13:29
@dsimansk

Copy link
Copy Markdown
Contributor

@kahirokunn is running multiple CRs of the same kind in parallel namespaces supported use case now? I.e. KnativeServing being on namespace: ns1, namespace: ns2 based on the same cluster profile?

@kahirokunn

Copy link
Copy Markdown
MemberAuthor

No, multiple CRs of the same kind targeting the same cluster are not intended to be supported.
I recalled the earlier discussion that each cluster supports only one Serving and one Eventing installation.
#1472 (comment)

Therefore, allowing an arbitrary destination namespace may be the wrong API design. The management CR may live in any hub namespace, while the remote installation namespace should probably be derived from the kind (knative-serving or knative-eventing). We should also prevent multiple CRs of the same kind from targeting the same ClusterProfile.

@kahirokunn
kahirokunn marked this pull request as draft August 26, 2026 02:16
@knative-prowknative-prowBot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 26, 2026
@kahirokunn

Copy link
Copy Markdown
MemberAuthor

Move to #2349

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge/work-in-progressIndicates that a PR should not merge because it is a work in progress.size/XLDenotes a PR that changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Install remote Knative components in canonical namespaces

2 participants

@kahirokunn@dsimansk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Support explicit remote namespaces via spec.destination - #2344

Closed
kahirokunn wants to merge 2 commits into
knative:mainfrom
kahirokunn:multicluster-destination
Closed

Support explicit remote namespaces via spec.destination#2344
kahirokunn wants to merge 2 commits into
knative:mainfrom
kahirokunn:multicluster-destination

Conversation

@kahirokunn

@kahirokunnkahirokunn commented Aug 22, 2026

Copy link
Copy Markdown
Member

Fixes#2345

Proposed Changes

  • Replace the top-level spec.clusterProfileRef field with an explicit spec.destination object containing both clusterProfileRef and the remote installation namespace.

Design

This PR makes the destination explicit:

spec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

The complete spec.destination is immutable. Moving an existing installation between clusters or namespaces requires deleting and recreating the management CR so that remote finalization runs before the new destination is installed.

Example CR

KnativeServing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-serving-apacnamespace: management-servingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-servingingress:
gateway-api:
enabled: trueistio:
enabled: false

KnativeEventing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeEventingmetadata:
name: knative-eventing-apacnamespace: management-eventingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-eventing

Local cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-servingnamespace: knative-servingspec: {}# No destination: deploys to the local cluster as before.

Verification Steps

E2E verification with Kind

Verified with three Kind clusters (hub, cluster1, and cluster2), OCM-managed ClusterProfile objects and cluster-proxy endpoints, the managed-serviceaccount ClusterProfileCredSyncer, cp-creds, Envoy Gateway, and cloud-provider-kind.

1. Set up OCM and Cluster Inventory API

The environment follows the OCM ClusterProfile guide: three Kind clusters, both spokes registered with OCM, the sandbox-fleet ManagedClusterSet, cluster-proxy, managed-serviceaccount, and ClusterProfile support.

Enable ClusterProfile credential synchronization in the managed-serviceaccount add-on:

helm upgrade managed-serviceaccount ocm/managed-serviceaccount \
--kube-context kind-hub \
-n open-cluster-management-managed-serviceaccount \
--version 0.10.0 \
--reuse-values \
--set featureGates.clusterProfileCredSyncer=true

Bind the operator namespace to the ManagedClusterSet and label both ManagedServiceAccounts for credential synchronization:

apiVersion: cluster.open-cluster-management.io/v1beta2kind: ManagedClusterSetBindingmetadata:
name: sandbox-fleetnamespace: knative-operatorspec:
clusterSet: sandbox-fleet
kubectl --context kind-hub -n cluster1 label managedserviceaccount knative-operator \
authentication.open-cluster-management.io/sync-to-clusterprofile=true
kubectl --context kind-hub -n cluster2 label managedserviceaccount knative-operator \
authentication.open-cluster-management.io/sync-to-clusterprofile=true

Both ClusterProfiles reported ControlPlaneHealthy=True and Joined=True. The managed-serviceaccount add-on synchronized their credentials into the knative-operator namespace.

2. Configure the operator to use cp-creds

The operator mounted quay.io/open-cluster-management/cp-creds:latest as an image volume at /access-plugins/cp-creds and used this provider configuration:

{
"providers": [
{
"name": "open-cluster-management",
"execConfig": {
"apiVersion": "client.authentication.k8s.io/v1",
"command": "/access-plugins/cp-creds/cp-creds",
"args": ["--managed-serviceaccount=knative-operator"],
"provideClusterInfo": true,
"interactiveMode": "Never"
}
}
]
}

3. Build and deploy this PR head

PATH="$(go env GOPATH)/bin:$PATH" \
KO_DOCKER_REPO=kind.local \
KIND_CLUSTER_NAME=hub \
ko apply -f config/
kubectl --context kind-hub -n knative-operator \
rollout status deployment/knative-operator --timeout=180s

Both destination ClusterProfiles resolved through the configured cp-creds provider.

4. Configure the Serving data plane on cluster1

Gateway API v1.4.1 experimental CRDs and Envoy Gateway v1.7.1 were installed on cluster1 using the same GatewayNamespace deployment model as the original multicluster PR. Both GatewayClasses reported Accepted=True, and both Gateways reported Programmed=True.

5. Deploy Serving and Eventing to explicit remote namespaces

The management CR namespaces intentionally differ from the remote installation namespaces:

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: ocm-servingnamespace: management-serving-hubspec:
destination:
clusterProfileRef:
name: cluster1namespace: knative-operatornamespace: knative-servingnamespace:
labels:
operator.knative.dev/ocm-e2e: cluster1annotations:
operator.knative.dev/ocm-e2e-source: destinationingress:
gateway-api:
enabled: trueistio:
enabled: false
---
apiVersion: operator.knative.dev/v1beta1kind: KnativeEventingmetadata:
name: ocm-eventingnamespace: management-eventing-hubspec:
destination:
clusterProfileRef:
name: cluster2namespace: knative-operatornamespace: knative-eventingnamespace:
labels:
operator.knative.dev/ocm-e2e: cluster2annotations:
operator.knative.dev/ocm-e2e-source: destination

Both hub CRs reached TargetClusterResolved=True and Ready=True. All seven Serving Deployments became Available only in cluster1/knative-serving; Knative Eventing reached Ready=True with its controller, webhook, and broker Deployments only in cluster2/knative-eventing; no Knative Deployments appeared in either hub management namespace or on the wrong spoke. The remote namespace metadata was applied, and every managed Deployment was owned by its remote anchor ConfigMap.

6. Verify Serving traffic

kubectl --context kind-cluster1 apply -f - <<'EOF'apiVersion: serving.knative.dev/v1kind: Servicemetadata: name: helloworld-go namespace: defaultspec: template: spec: containers: - image: gcr.io/knative-samples/helloworld-go env: - name: TARGET value: OCM Native cp-creds E2EEOF
kubectl --context kind-cluster1 -n default \
wait --for=condition=Ready kservice/helloworld-go --timeout=300s
LB_IP=$(kubectl --context kind-cluster1 -n eg-external \ get service knative-external \ -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -H 'Host: helloworld-go.default.example.com'"http://${LB_IP}"

Observed response:

Hello OCM Native cp-creds E2E!

The external and cluster-local HTTPRoutes both reported Accepted=True and ResolvedRefs=True.

7. Verify cleanup with a cold cache

To exercise finalization before the ClusterProfile cache was warm, the operator was restarted and both management CRs were deleted immediately. Both remote anchors and their owned Deployments were removed, and both CRs completed deletion.

After recreating the CRs in the same destination namespaces, both returned to TargetClusterResolved=True and Ready=True.

Breaking change

Existing manifests using the unreleased top-level field must be rewritten:

# Beforespec:
clusterProfileRef:
name: cluster1namespace: cluster-inventory# Afterspec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

Release Note

action required: Replace spec.clusterProfileRef with spec.destination.clusterProfileRef and set spec.destination.namespace for remote Knative installations.

@knative-prowknative-prowBot added the size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. label Aug 22, 2026

@knative-prowknative-prowBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kahirokunn: 0 warnings.

Details

In response to this:

Proposed Changes

  • Replace the top-level spec.clusterProfileRef field with an explicit
    spec.destination object containing both clusterProfileRef and the remote
    installation namespace.
  • Reconcile remote manifests, ingress resources, and the anchor ConfigMap into
    spec.destination.namespace instead of coupling the remote installation to
    the management CR namespace.
  • Make the complete destination immutable and validate the ClusterProfile name,
    ClusterProfile namespace, and installation namespace as Kubernetes names.
  • Update generated CRDs, Helm CRDs, documentation, unit tests, and multicluster
    E2E coverage for the new API shape.

This is an intentional breaking replacement of an unreleased API with no
current consumers. No compatibility or migration path for the removed
top-level spec.clusterProfileRef field is retained.

Design

The existing multicluster reconciliation design remains unchanged: the
operator resolves the referenced Cluster Inventory API ClusterProfile, swaps
the manifest client at the start of reconciliation, and uses an anchor
ConfigMap for namespace-scoped garbage collection on the remote cluster.

This PR makes the destination explicit:

spec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

The management CR namespace and remote installation namespace are independent.
spec.namespace.labels and spec.namespace.annotations are merged into the
selected remote namespace while preserving unrelated existing metadata.

The complete spec.destination is immutable. Moving an existing installation
between clusters or namespaces requires deleting and recreating the management
CR so that remote finalization runs before the new destination is installed.

Example CR

KnativeServing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-serving-apacnamespace: management-servingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-servingingress:
gateway-api:
enabled: trueistio:
enabled: false

KnativeEventing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeEventingmetadata:
name: knative-eventing-apacnamespace: management-eventingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-eventing

Local cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-servingnamespace: knative-servingspec: {}# No destination: deploys to the local cluster as before.

Verification Steps

Automated verification

go test ./...
go test -run '^$' -tags='e2e multicluster' ./test/e2e
./hack/verify-codegen.sh

The repository multicluster Kind suite also passed all 11 tests:

SPOKE_CLUSTER_NAME=codex-destination-spoke \
./test/e2e-tests-multicluster.sh --kubeconfig=<hub-kubeconfig>

E2E verification with Kind

The following end-to-end flow was executed against this PR head. It uses three
Kind clusters (hub, cluster1, and cluster2), real OCM-managed
ClusterProfile objects, the official Cluster Inventory API secretreader
plugin, Envoy Gateway, and cloud-provider-kind.

1. Cluster and OCM setup

The OCM and Cluster Inventory API environment was created following the
OCM ClusterProfile guide:

kind create cluster --name hub
kind create cluster --name cluster1
kind create cluster --name cluster2
# Install OCM on hub, join cluster1 and cluster2, and enable ClusterProfile.# Install sandbox-fleet, cluster-proxy, and managed-serviceaccount.
kubectl --context kind-hub get managedclusters
kubectl --context kind-hub -n cluster-inventory get clusterprofiles
kubectl --context kind-hub get managedclusteraddons -A

Verified state:

  • cluster1 and cluster2: Joined=True, Available=True
  • generated ClusterProfiles: ControlPlaneHealthy=True, Joined=True
  • cluster-proxy and managed-serviceaccount: Available=True

2. Spoke credentials and ClusterProfile access

An OCM ManagedServiceAccount named knative-operator was created for each
managed cluster. The generated ClusterProfiles expose the cluster-proxy access
provider:

kubectl --context kind-hub -n cluster1 get managedserviceaccount knative-operator
kubectl --context kind-hub -n cluster-inventory get clusterprofile cluster1 -o yaml

The operator uses the official secretreader plugin:

{
"providers": [
{
"name": "open-cluster-management",
"execConfig": {
"apiVersion": "client.authentication.k8s.io/v1",
"command": "/access-plugins/secretreader/bin/secretreader-plugin",
"provideClusterInfo": true,
"interactiveMode": "Never"
}
}
]
}

The plugin image was
registry.k8s.io/cluster-inventory-api/secretreader:v0.1.3.

3. Operator deployment

PATH="$(go env GOPATH)/bin:$PATH" \
KO_DOCKER_REPO=kind.local \
KIND_CLUSTER_NAME=hub \
ko apply -f config/
kubectl --context kind-hub -n knative-operator \
rollout status deployment/knative-operator --timeout=180s

The operator was configured with:

--clusterprofile-provider-file=/etc/cluster-inventory/config.json
--remote-deployments-poll-interval=2s

The running operator reported commit 418c364 and successfully resolved the
OCM cluster-proxy endpoint for cluster-inventory/cluster1.

4. Install Envoy Gateway on cluster1

Gateway API v1.4.1 experimental CRDs were installed on the spoke. Envoy Gateway
v1.7.1 was then installed using the same GatewayNamespace deployment model as
the original multicluster PR:

cat <<'EOF' > /tmp/values-eg.yamlconfig: envoyGateway: provider: type: Kubernetes kubernetes: deploy: type: GatewayNamespaceEOF
kubectl --context kind-cluster1 create namespace envoy-gateway-system
helm template eg oci://docker.io/envoyproxy/gateway-helm \
--version v1.7.1 \
-n envoy-gateway-system \
-f /tmp/values-eg.yaml \
--include-crds | \
sed -n '/^---$/,$p'| \
kubectl --context kind-cluster1 apply --server-side --force-conflicts -f -
sudo cloud-provider-kind

5. Create external and internal Gateway resources

Two Envoy Gateway instances were created:

  • eg-external/eg-external: LoadBalancer, ports 80 and 443
  • eg-internal/eg-internal: ClusterIP, port 80
apiVersion: v1kind: Namespacemetadata:
name: eg-external
---
apiVersion: gateway.envoyproxy.io/v1alpha1kind: EnvoyProxymetadata:
name: knative-external-confignamespace: envoy-gateway-systemspec:
provider:
type: Kuberneteskubernetes:
envoyService:
name: knative-external
---
apiVersion: gateway.networking.k8s.io/v1kind: GatewayClassmetadata:
name: eg-externalspec:
controllerName: gateway.envoyproxy.io/gatewayclass-controllerparametersRef:
group: gateway.envoyproxy.iokind: EnvoyProxyname: knative-external-confignamespace: envoy-gateway-system
---
apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata:
name: eg-externalnamespace: eg-externalspec:
gatewayClassName: eg-externallisteners:
- name: httpport: 80protocol: HTTPallowedRoutes:
namespaces:
from: All
- name: tlsport: 443protocol: TLStls:
mode: PassthroughallowedRoutes:
namespaces:
from: All
---
apiVersion: v1kind: Namespacemetadata:
name: eg-internal
---
apiVersion: gateway.envoyproxy.io/v1alpha1kind: EnvoyProxymetadata:
name: knative-internal-confignamespace: envoy-gateway-systemspec:
provider:
type: Kuberneteskubernetes:
envoyService:
type: ClusterIPname: knative-internal
---
apiVersion: gateway.networking.k8s.io/v1kind: GatewayClassmetadata:
name: eg-internalspec:
controllerName: gateway.envoyproxy.io/gatewayclass-controllerparametersRef:
group: gateway.envoyproxy.iokind: EnvoyProxyname: knative-internal-confignamespace: envoy-gateway-system
---
apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata:
name: eg-internalnamespace: eg-internalspec:
gatewayClassName: eg-internallisteners:
- name: httpport: 80protocol: HTTPallowedRoutes:
namespaces:
from: All

Both GatewayClasses reached Accepted=True; both Gateways reached
Programmed=True. The external service received 172.18.0.12 from
cloud-provider-kind.

6. Deploy KnativeServing with an explicit destination

kubectl --context kind-hub apply -f - <<'EOF'apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata: name: ocm-serving namespace: management-serving-hubspec: destination: clusterProfileRef: name: cluster1 namespace: cluster-inventory namespace: knative-serving-ocm ingress: gateway-api: enabled: true istio: enabled: false config: config-gateway: external-gateways: | - class: eg-external gateway: eg-external/eg-external service: eg-external/knative-external supported-features: - HTTPRouteRequestTimeout local-gateways: | - class: eg-internal gateway: eg-internal/eg-internal service: eg-internal/knative-internal supported-features: - HTTPRouteRequestTimeout network: ingress-class: gateway-api.ingress.networking.knative.dev domain: example.com: ""EOF

The hub CR reached Ready=True and TargetClusterResolved=True. All seven
Knative Serving Deployments became Available in
cluster1/knative-serving-ocm, with no Serving Deployment in the hub management
namespace or on cluster2.

7. Verify ownership and data-plane traffic

The remote anchor was present and protected:

kubectl --context kind-cluster1 -n knative-serving-ocm \
get configmap knativeserving-ocm-serving-root-owner -o yaml
kubectl --context kind-cluster1 -n knative-serving-ocm \
get deployment activator -o jsonpath='{.metadata.ownerReferences}'| jq .

The activator Deployment ownerReference matched the anchor ConfigMap UID.

A Knative Service was then deployed on cluster1:

kubectl --context kind-cluster1 apply -f - <<'EOF'apiVersion: serving.knative.dev/v1kind: Servicemetadata: name: helloworld-go namespace: defaultspec: template: spec: containers: - image: gcr.io/knative-samples/helloworld-go env: - name: TARGET value: Destination Kind E2EEOF
kubectl --context kind-cluster1 -n default \
wait --for=condition=Ready kservice/helloworld-go --timeout=300s
LB_IP=$(kubectl --context kind-cluster1 -n eg-external \ get service knative-external \ -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -H 'Host: helloworld-go.default.example.com'"http://${LB_IP}"

Observed response:

Hello Destination Kind E2E!

The external and cluster-local HTTPRoutes both reported Accepted=True.

8. Verify CR deletion and finalizer cleanup

kubectl --context kind-cluster1 -n default delete kservice helloworld-go
kubectl --context kind-hub -n management-serving-hub \
delete knativeserving ocm-serving

Verified after finalization:

  • the hub KnativeServing CR was deleted
  • the remote anchor ConfigMap was deleted
  • all labeled Knative Serving Deployments were deleted
  • all Knative Serving ClusterRoles were deleted

The dynamically created autoscaler-bucket-00-of-01 Service and leader-election
Leases do not carry the anchor ownerReference and can remain after uninstall.
This also occurs on fork/main and is not introduced by the destination API
replacement. Those test-only remnants were removed before reinstalling into the
same namespace.

9. Recreate and leave the environment healthy

The same KnativeServing CR was recreated after cleanup. It returned to
Ready=True and TargetClusterResolved=True, and all seven Serving Deployments
became Available again. The OCM hub, both managed clusters, Envoy Gateway,
Knative Serving, and Knative Eventing were left running for inspection.

Destination validation

The real API server rejected all of the following:

  • invalid destination namespace
  • missing ClusterProfile name, ClusterProfile namespace, or target namespace
  • legacy top-level spec.clusterProfileRef
  • changes to destination.namespace
  • changes to destination.clusterProfileRef.name
  • changes to destination.clusterProfileRef.namespace
  • removal of spec.destination

Intentional breaking change

Existing manifests using the unreleased top-level field must be rewritten:

# Beforespec:
clusterProfileRef:
name: cluster1namespace: cluster-inventory# Afterspec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

No automatic migration or compatibility field is provided because the old API
has no current consumers.

Release Note

action required: Replace spec.clusterProfileRef with spec.destination.clusterProfileRef and set spec.destination.namespace for remote Knative installations.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@knative-prow

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: kahirokunn
Once this PR has been reviewed and has the lgtm label, please assign dsimansk for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@codecov

codecovBot commented Aug 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.14286% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.54%. Comparing base (0fc378e) to head (a5ca744).
⚠️ Report is 2 commits behind head on main.

Files with missing linesPatch %Lines
pkg/apis/operator/base/common.go0.00%2 Missing ⚠️
pkg/reconciler/common/multicluster.go94.73%1 Missing ⚠️
pkg/reconciler/knativeeventing/controller.go0.00%1 Missing ⚠️
pkg/reconciler/knativeserving/controller.go0.00%1 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #2344 +/- ##
==========================================
+ Coverage 63.99% 64.54% +0.55% 
==========================================
Files 55 55 Lines 2491 2502 +11 ==========================================
+ Hits 1594 1615 +21 + Misses 777 761 -16 - Partials 120 126 +6 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@kahirokunn

Copy link
Copy Markdown
MemberAuthor

/retest

@kahirokunn
kahirokunn marked this pull request as draft August 23, 2026 00:47
@knative-prowknative-prowBot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 23, 2026
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 418c364 to 449028eCompareAugust 23, 2026 00:54
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 449028e to 7345ad9CompareAugust 23, 2026 01:11
Signed-off-by: kahirokunn <okinakahiro@gmail.com>
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 7345ad9 to 8447d27CompareAugust 23, 2026 13:24
@kahirokunn
kahirokunn marked this pull request as ready for review August 23, 2026 13:28
@knative-prowknative-prowBot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 23, 2026
@knative-prow
knative-prowBot requested a review from matzewAugust 23, 2026 13:28
Signed-off-by: kahirokunn <okinakahiro@gmail.com>
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 8447d27 to a5ca744CompareAugust 23, 2026 13:29
@dsimansk

Copy link
Copy Markdown
Contributor

@kahirokunn is running multiple CRs of the same kind in parallel namespaces supported use case now? I.e. KnativeServing being on namespace: ns1, namespace: ns2 based on the same cluster profile?

@kahirokunn

Copy link
Copy Markdown
MemberAuthor

No, multiple CRs of the same kind targeting the same cluster are not intended to be supported.
I recalled the earlier discussion that each cluster supports only one Serving and one Eventing installation.
#1472 (comment)

Therefore, allowing an arbitrary destination namespace may be the wrong API design. The management CR may live in any hub namespace, while the remote installation namespace should probably be derived from the kind (knative-serving or knative-eventing). We should also prevent multiple CRs of the same kind from targeting the same ClusterProfile.

@kahirokunn
kahirokunn marked this pull request as draft August 26, 2026 02:16
@knative-prowknative-prowBot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 26, 2026
@kahirokunn

Copy link
Copy Markdown
MemberAuthor

Move to #2349

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge/work-in-progressIndicates that a PR should not merge because it is a work in progress.size/XLDenotes a PR that changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Install remote Knative components in canonical namespaces

2 participants

@kahirokunn@dsimansk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Support explicit remote namespaces via spec.destination - #2344

Closed
kahirokunn wants to merge 2 commits into
knative:mainfrom
kahirokunn:multicluster-destination
Closed

Support explicit remote namespaces via spec.destination#2344
kahirokunn wants to merge 2 commits into
knative:mainfrom
kahirokunn:multicluster-destination

Conversation

@kahirokunn

@kahirokunnkahirokunn commented Aug 22, 2026

Copy link
Copy Markdown
Member

Fixes#2345

Proposed Changes

  • Replace the top-level spec.clusterProfileRef field with an explicit spec.destination object containing both clusterProfileRef and the remote installation namespace.

Design

This PR makes the destination explicit:

spec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

The complete spec.destination is immutable. Moving an existing installation between clusters or namespaces requires deleting and recreating the management CR so that remote finalization runs before the new destination is installed.

Example CR

KnativeServing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-serving-apacnamespace: management-servingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-servingingress:
gateway-api:
enabled: trueistio:
enabled: false

KnativeEventing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeEventingmetadata:
name: knative-eventing-apacnamespace: management-eventingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-eventing

Local cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-servingnamespace: knative-servingspec: {}# No destination: deploys to the local cluster as before.

Verification Steps

E2E verification with Kind

Verified with three Kind clusters (hub, cluster1, and cluster2), OCM-managed ClusterProfile objects and cluster-proxy endpoints, the managed-serviceaccount ClusterProfileCredSyncer, cp-creds, Envoy Gateway, and cloud-provider-kind.

1. Set up OCM and Cluster Inventory API

The environment follows the OCM ClusterProfile guide: three Kind clusters, both spokes registered with OCM, the sandbox-fleet ManagedClusterSet, cluster-proxy, managed-serviceaccount, and ClusterProfile support.

Enable ClusterProfile credential synchronization in the managed-serviceaccount add-on:

helm upgrade managed-serviceaccount ocm/managed-serviceaccount \
--kube-context kind-hub \
-n open-cluster-management-managed-serviceaccount \
--version 0.10.0 \
--reuse-values \
--set featureGates.clusterProfileCredSyncer=true

Bind the operator namespace to the ManagedClusterSet and label both ManagedServiceAccounts for credential synchronization:

apiVersion: cluster.open-cluster-management.io/v1beta2kind: ManagedClusterSetBindingmetadata:
name: sandbox-fleetnamespace: knative-operatorspec:
clusterSet: sandbox-fleet
kubectl --context kind-hub -n cluster1 label managedserviceaccount knative-operator \
authentication.open-cluster-management.io/sync-to-clusterprofile=true
kubectl --context kind-hub -n cluster2 label managedserviceaccount knative-operator \
authentication.open-cluster-management.io/sync-to-clusterprofile=true

Both ClusterProfiles reported ControlPlaneHealthy=True and Joined=True. The managed-serviceaccount add-on synchronized their credentials into the knative-operator namespace.

2. Configure the operator to use cp-creds

The operator mounted quay.io/open-cluster-management/cp-creds:latest as an image volume at /access-plugins/cp-creds and used this provider configuration:

{
"providers": [
{
"name": "open-cluster-management",
"execConfig": {
"apiVersion": "client.authentication.k8s.io/v1",
"command": "/access-plugins/cp-creds/cp-creds",
"args": ["--managed-serviceaccount=knative-operator"],
"provideClusterInfo": true,
"interactiveMode": "Never"
}
}
]
}

3. Build and deploy this PR head

PATH="$(go env GOPATH)/bin:$PATH" \
KO_DOCKER_REPO=kind.local \
KIND_CLUSTER_NAME=hub \
ko apply -f config/
kubectl --context kind-hub -n knative-operator \
rollout status deployment/knative-operator --timeout=180s

Both destination ClusterProfiles resolved through the configured cp-creds provider.

4. Configure the Serving data plane on cluster1

Gateway API v1.4.1 experimental CRDs and Envoy Gateway v1.7.1 were installed on cluster1 using the same GatewayNamespace deployment model as the original multicluster PR. Both GatewayClasses reported Accepted=True, and both Gateways reported Programmed=True.

5. Deploy Serving and Eventing to explicit remote namespaces

The management CR namespaces intentionally differ from the remote installation namespaces:

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: ocm-servingnamespace: management-serving-hubspec:
destination:
clusterProfileRef:
name: cluster1namespace: knative-operatornamespace: knative-servingnamespace:
labels:
operator.knative.dev/ocm-e2e: cluster1annotations:
operator.knative.dev/ocm-e2e-source: destinationingress:
gateway-api:
enabled: trueistio:
enabled: false
---
apiVersion: operator.knative.dev/v1beta1kind: KnativeEventingmetadata:
name: ocm-eventingnamespace: management-eventing-hubspec:
destination:
clusterProfileRef:
name: cluster2namespace: knative-operatornamespace: knative-eventingnamespace:
labels:
operator.knative.dev/ocm-e2e: cluster2annotations:
operator.knative.dev/ocm-e2e-source: destination

Both hub CRs reached TargetClusterResolved=True and Ready=True. All seven Serving Deployments became Available only in cluster1/knative-serving; Knative Eventing reached Ready=True with its controller, webhook, and broker Deployments only in cluster2/knative-eventing; no Knative Deployments appeared in either hub management namespace or on the wrong spoke. The remote namespace metadata was applied, and every managed Deployment was owned by its remote anchor ConfigMap.

6. Verify Serving traffic

kubectl --context kind-cluster1 apply -f - <<'EOF'apiVersion: serving.knative.dev/v1kind: Servicemetadata: name: helloworld-go namespace: defaultspec: template: spec: containers: - image: gcr.io/knative-samples/helloworld-go env: - name: TARGET value: OCM Native cp-creds E2EEOF
kubectl --context kind-cluster1 -n default \
wait --for=condition=Ready kservice/helloworld-go --timeout=300s
LB_IP=$(kubectl --context kind-cluster1 -n eg-external \ get service knative-external \ -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -H 'Host: helloworld-go.default.example.com'"http://${LB_IP}"

Observed response:

Hello OCM Native cp-creds E2E!

The external and cluster-local HTTPRoutes both reported Accepted=True and ResolvedRefs=True.

7. Verify cleanup with a cold cache

To exercise finalization before the ClusterProfile cache was warm, the operator was restarted and both management CRs were deleted immediately. Both remote anchors and their owned Deployments were removed, and both CRs completed deletion.

After recreating the CRs in the same destination namespaces, both returned to TargetClusterResolved=True and Ready=True.

Breaking change

Existing manifests using the unreleased top-level field must be rewritten:

# Beforespec:
clusterProfileRef:
name: cluster1namespace: cluster-inventory# Afterspec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

Release Note

action required: Replace spec.clusterProfileRef with spec.destination.clusterProfileRef and set spec.destination.namespace for remote Knative installations.

@knative-prowknative-prowBot added the size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. label Aug 22, 2026

@knative-prowknative-prowBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kahirokunn: 0 warnings.

Details

In response to this:

Proposed Changes

  • Replace the top-level spec.clusterProfileRef field with an explicit
    spec.destination object containing both clusterProfileRef and the remote
    installation namespace.
  • Reconcile remote manifests, ingress resources, and the anchor ConfigMap into
    spec.destination.namespace instead of coupling the remote installation to
    the management CR namespace.
  • Make the complete destination immutable and validate the ClusterProfile name,
    ClusterProfile namespace, and installation namespace as Kubernetes names.
  • Update generated CRDs, Helm CRDs, documentation, unit tests, and multicluster
    E2E coverage for the new API shape.

This is an intentional breaking replacement of an unreleased API with no
current consumers. No compatibility or migration path for the removed
top-level spec.clusterProfileRef field is retained.

Design

The existing multicluster reconciliation design remains unchanged: the
operator resolves the referenced Cluster Inventory API ClusterProfile, swaps
the manifest client at the start of reconciliation, and uses an anchor
ConfigMap for namespace-scoped garbage collection on the remote cluster.

This PR makes the destination explicit:

spec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

The management CR namespace and remote installation namespace are independent.
spec.namespace.labels and spec.namespace.annotations are merged into the
selected remote namespace while preserving unrelated existing metadata.

The complete spec.destination is immutable. Moving an existing installation
between clusters or namespaces requires deleting and recreating the management
CR so that remote finalization runs before the new destination is installed.

Example CR

KnativeServing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-serving-apacnamespace: management-servingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-servingingress:
gateway-api:
enabled: trueistio:
enabled: false

KnativeEventing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeEventingmetadata:
name: knative-eventing-apacnamespace: management-eventingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-eventing

Local cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-servingnamespace: knative-servingspec: {}# No destination: deploys to the local cluster as before.

Verification Steps

Automated verification

go test ./...
go test -run '^$' -tags='e2e multicluster' ./test/e2e
./hack/verify-codegen.sh

The repository multicluster Kind suite also passed all 11 tests:

SPOKE_CLUSTER_NAME=codex-destination-spoke \
./test/e2e-tests-multicluster.sh --kubeconfig=<hub-kubeconfig>

E2E verification with Kind

The following end-to-end flow was executed against this PR head. It uses three
Kind clusters (hub, cluster1, and cluster2), real OCM-managed
ClusterProfile objects, the official Cluster Inventory API secretreader
plugin, Envoy Gateway, and cloud-provider-kind.

1. Cluster and OCM setup

The OCM and Cluster Inventory API environment was created following the
OCM ClusterProfile guide:

kind create cluster --name hub
kind create cluster --name cluster1
kind create cluster --name cluster2
# Install OCM on hub, join cluster1 and cluster2, and enable ClusterProfile.# Install sandbox-fleet, cluster-proxy, and managed-serviceaccount.
kubectl --context kind-hub get managedclusters
kubectl --context kind-hub -n cluster-inventory get clusterprofiles
kubectl --context kind-hub get managedclusteraddons -A

Verified state:

  • cluster1 and cluster2: Joined=True, Available=True
  • generated ClusterProfiles: ControlPlaneHealthy=True, Joined=True
  • cluster-proxy and managed-serviceaccount: Available=True

2. Spoke credentials and ClusterProfile access

An OCM ManagedServiceAccount named knative-operator was created for each
managed cluster. The generated ClusterProfiles expose the cluster-proxy access
provider:

kubectl --context kind-hub -n cluster1 get managedserviceaccount knative-operator
kubectl --context kind-hub -n cluster-inventory get clusterprofile cluster1 -o yaml

The operator uses the official secretreader plugin:

{
"providers": [
{
"name": "open-cluster-management",
"execConfig": {
"apiVersion": "client.authentication.k8s.io/v1",
"command": "/access-plugins/secretreader/bin/secretreader-plugin",
"provideClusterInfo": true,
"interactiveMode": "Never"
}
}
]
}

The plugin image was
registry.k8s.io/cluster-inventory-api/secretreader:v0.1.3.

3. Operator deployment

PATH="$(go env GOPATH)/bin:$PATH" \
KO_DOCKER_REPO=kind.local \
KIND_CLUSTER_NAME=hub \
ko apply -f config/
kubectl --context kind-hub -n knative-operator \
rollout status deployment/knative-operator --timeout=180s

The operator was configured with:

--clusterprofile-provider-file=/etc/cluster-inventory/config.json
--remote-deployments-poll-interval=2s

The running operator reported commit 418c364 and successfully resolved the
OCM cluster-proxy endpoint for cluster-inventory/cluster1.

4. Install Envoy Gateway on cluster1

Gateway API v1.4.1 experimental CRDs were installed on the spoke. Envoy Gateway
v1.7.1 was then installed using the same GatewayNamespace deployment model as
the original multicluster PR:

cat <<'EOF' > /tmp/values-eg.yamlconfig: envoyGateway: provider: type: Kubernetes kubernetes: deploy: type: GatewayNamespaceEOF
kubectl --context kind-cluster1 create namespace envoy-gateway-system
helm template eg oci://docker.io/envoyproxy/gateway-helm \
--version v1.7.1 \
-n envoy-gateway-system \
-f /tmp/values-eg.yaml \
--include-crds | \
sed -n '/^---$/,$p'| \
kubectl --context kind-cluster1 apply --server-side --force-conflicts -f -
sudo cloud-provider-kind

5. Create external and internal Gateway resources

Two Envoy Gateway instances were created:

  • eg-external/eg-external: LoadBalancer, ports 80 and 443
  • eg-internal/eg-internal: ClusterIP, port 80
apiVersion: v1kind: Namespacemetadata:
name: eg-external
---
apiVersion: gateway.envoyproxy.io/v1alpha1kind: EnvoyProxymetadata:
name: knative-external-confignamespace: envoy-gateway-systemspec:
provider:
type: Kuberneteskubernetes:
envoyService:
name: knative-external
---
apiVersion: gateway.networking.k8s.io/v1kind: GatewayClassmetadata:
name: eg-externalspec:
controllerName: gateway.envoyproxy.io/gatewayclass-controllerparametersRef:
group: gateway.envoyproxy.iokind: EnvoyProxyname: knative-external-confignamespace: envoy-gateway-system
---
apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata:
name: eg-externalnamespace: eg-externalspec:
gatewayClassName: eg-externallisteners:
- name: httpport: 80protocol: HTTPallowedRoutes:
namespaces:
from: All
- name: tlsport: 443protocol: TLStls:
mode: PassthroughallowedRoutes:
namespaces:
from: All
---
apiVersion: v1kind: Namespacemetadata:
name: eg-internal
---
apiVersion: gateway.envoyproxy.io/v1alpha1kind: EnvoyProxymetadata:
name: knative-internal-confignamespace: envoy-gateway-systemspec:
provider:
type: Kuberneteskubernetes:
envoyService:
type: ClusterIPname: knative-internal
---
apiVersion: gateway.networking.k8s.io/v1kind: GatewayClassmetadata:
name: eg-internalspec:
controllerName: gateway.envoyproxy.io/gatewayclass-controllerparametersRef:
group: gateway.envoyproxy.iokind: EnvoyProxyname: knative-internal-confignamespace: envoy-gateway-system
---
apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata:
name: eg-internalnamespace: eg-internalspec:
gatewayClassName: eg-internallisteners:
- name: httpport: 80protocol: HTTPallowedRoutes:
namespaces:
from: All

Both GatewayClasses reached Accepted=True; both Gateways reached
Programmed=True. The external service received 172.18.0.12 from
cloud-provider-kind.

6. Deploy KnativeServing with an explicit destination

kubectl --context kind-hub apply -f - <<'EOF'apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata: name: ocm-serving namespace: management-serving-hubspec: destination: clusterProfileRef: name: cluster1 namespace: cluster-inventory namespace: knative-serving-ocm ingress: gateway-api: enabled: true istio: enabled: false config: config-gateway: external-gateways: | - class: eg-external gateway: eg-external/eg-external service: eg-external/knative-external supported-features: - HTTPRouteRequestTimeout local-gateways: | - class: eg-internal gateway: eg-internal/eg-internal service: eg-internal/knative-internal supported-features: - HTTPRouteRequestTimeout network: ingress-class: gateway-api.ingress.networking.knative.dev domain: example.com: ""EOF

The hub CR reached Ready=True and TargetClusterResolved=True. All seven
Knative Serving Deployments became Available in
cluster1/knative-serving-ocm, with no Serving Deployment in the hub management
namespace or on cluster2.

7. Verify ownership and data-plane traffic

The remote anchor was present and protected:

kubectl --context kind-cluster1 -n knative-serving-ocm \
get configmap knativeserving-ocm-serving-root-owner -o yaml
kubectl --context kind-cluster1 -n knative-serving-ocm \
get deployment activator -o jsonpath='{.metadata.ownerReferences}'| jq .

The activator Deployment ownerReference matched the anchor ConfigMap UID.

A Knative Service was then deployed on cluster1:

kubectl --context kind-cluster1 apply -f - <<'EOF'apiVersion: serving.knative.dev/v1kind: Servicemetadata: name: helloworld-go namespace: defaultspec: template: spec: containers: - image: gcr.io/knative-samples/helloworld-go env: - name: TARGET value: Destination Kind E2EEOF
kubectl --context kind-cluster1 -n default \
wait --for=condition=Ready kservice/helloworld-go --timeout=300s
LB_IP=$(kubectl --context kind-cluster1 -n eg-external \ get service knative-external \ -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -H 'Host: helloworld-go.default.example.com'"http://${LB_IP}"

Observed response:

Hello Destination Kind E2E!

The external and cluster-local HTTPRoutes both reported Accepted=True.

8. Verify CR deletion and finalizer cleanup

kubectl --context kind-cluster1 -n default delete kservice helloworld-go
kubectl --context kind-hub -n management-serving-hub \
delete knativeserving ocm-serving

Verified after finalization:

  • the hub KnativeServing CR was deleted
  • the remote anchor ConfigMap was deleted
  • all labeled Knative Serving Deployments were deleted
  • all Knative Serving ClusterRoles were deleted

The dynamically created autoscaler-bucket-00-of-01 Service and leader-election
Leases do not carry the anchor ownerReference and can remain after uninstall.
This also occurs on fork/main and is not introduced by the destination API
replacement. Those test-only remnants were removed before reinstalling into the
same namespace.

9. Recreate and leave the environment healthy

The same KnativeServing CR was recreated after cleanup. It returned to
Ready=True and TargetClusterResolved=True, and all seven Serving Deployments
became Available again. The OCM hub, both managed clusters, Envoy Gateway,
Knative Serving, and Knative Eventing were left running for inspection.

Destination validation

The real API server rejected all of the following:

  • invalid destination namespace
  • missing ClusterProfile name, ClusterProfile namespace, or target namespace
  • legacy top-level spec.clusterProfileRef
  • changes to destination.namespace
  • changes to destination.clusterProfileRef.name
  • changes to destination.clusterProfileRef.namespace
  • removal of spec.destination

Intentional breaking change

Existing manifests using the unreleased top-level field must be rewritten:

# Beforespec:
clusterProfileRef:
name: cluster1namespace: cluster-inventory# Afterspec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

No automatic migration or compatibility field is provided because the old API
has no current consumers.

Release Note

action required: Replace spec.clusterProfileRef with spec.destination.clusterProfileRef and set spec.destination.namespace for remote Knative installations.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@knative-prow

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: kahirokunn
Once this PR has been reviewed and has the lgtm label, please assign dsimansk for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@codecov

codecovBot commented Aug 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.14286% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.54%. Comparing base (0fc378e) to head (a5ca744).
⚠️ Report is 2 commits behind head on main.

Files with missing linesPatch %Lines
pkg/apis/operator/base/common.go0.00%2 Missing ⚠️
pkg/reconciler/common/multicluster.go94.73%1 Missing ⚠️
pkg/reconciler/knativeeventing/controller.go0.00%1 Missing ⚠️
pkg/reconciler/knativeserving/controller.go0.00%1 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #2344 +/- ##
==========================================
+ Coverage 63.99% 64.54% +0.55% 
==========================================
Files 55 55 Lines 2491 2502 +11 ==========================================
+ Hits 1594 1615 +21 + Misses 777 761 -16 - Partials 120 126 +6 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@kahirokunn

Copy link
Copy Markdown
MemberAuthor

/retest

@kahirokunn
kahirokunn marked this pull request as draft August 23, 2026 00:47
@knative-prowknative-prowBot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 23, 2026
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 418c364 to 449028eCompareAugust 23, 2026 00:54
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 449028e to 7345ad9CompareAugust 23, 2026 01:11
Signed-off-by: kahirokunn <okinakahiro@gmail.com>
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 7345ad9 to 8447d27CompareAugust 23, 2026 13:24
@kahirokunn
kahirokunn marked this pull request as ready for review August 23, 2026 13:28
@knative-prowknative-prowBot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 23, 2026
@knative-prow
knative-prowBot requested a review from matzewAugust 23, 2026 13:28
Signed-off-by: kahirokunn <okinakahiro@gmail.com>
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 8447d27 to a5ca744CompareAugust 23, 2026 13:29
@dsimansk

Copy link
Copy Markdown
Contributor

@kahirokunn is running multiple CRs of the same kind in parallel namespaces supported use case now? I.e. KnativeServing being on namespace: ns1, namespace: ns2 based on the same cluster profile?

@kahirokunn

Copy link
Copy Markdown
MemberAuthor

No, multiple CRs of the same kind targeting the same cluster are not intended to be supported.
I recalled the earlier discussion that each cluster supports only one Serving and one Eventing installation.
#1472 (comment)

Therefore, allowing an arbitrary destination namespace may be the wrong API design. The management CR may live in any hub namespace, while the remote installation namespace should probably be derived from the kind (knative-serving or knative-eventing). We should also prevent multiple CRs of the same kind from targeting the same ClusterProfile.

@kahirokunn
kahirokunn marked this pull request as draft August 26, 2026 02:16
@knative-prowknative-prowBot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 26, 2026
@kahirokunn

Copy link
Copy Markdown
MemberAuthor

Move to #2349

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge/work-in-progressIndicates that a PR should not merge because it is a work in progress.size/XLDenotes a PR that changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Install remote Knative components in canonical namespaces

2 participants

@kahirokunn@dsimansk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Support explicit remote namespaces via spec.destination - #2344

Closed
kahirokunn wants to merge 2 commits into
knative:mainfrom
kahirokunn:multicluster-destination
Closed

Support explicit remote namespaces via spec.destination#2344
kahirokunn wants to merge 2 commits into
knative:mainfrom
kahirokunn:multicluster-destination

Conversation

@kahirokunn

@kahirokunnkahirokunn commented Aug 22, 2026

Copy link
Copy Markdown
Member

Fixes#2345

Proposed Changes

  • Replace the top-level spec.clusterProfileRef field with an explicit spec.destination object containing both clusterProfileRef and the remote installation namespace.

Design

This PR makes the destination explicit:

spec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

The complete spec.destination is immutable. Moving an existing installation between clusters or namespaces requires deleting and recreating the management CR so that remote finalization runs before the new destination is installed.

Example CR

KnativeServing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-serving-apacnamespace: management-servingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-servingingress:
gateway-api:
enabled: trueistio:
enabled: false

KnativeEventing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeEventingmetadata:
name: knative-eventing-apacnamespace: management-eventingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-eventing

Local cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-servingnamespace: knative-servingspec: {}# No destination: deploys to the local cluster as before.

Verification Steps

E2E verification with Kind

Verified with three Kind clusters (hub, cluster1, and cluster2), OCM-managed ClusterProfile objects and cluster-proxy endpoints, the managed-serviceaccount ClusterProfileCredSyncer, cp-creds, Envoy Gateway, and cloud-provider-kind.

1. Set up OCM and Cluster Inventory API

The environment follows the OCM ClusterProfile guide: three Kind clusters, both spokes registered with OCM, the sandbox-fleet ManagedClusterSet, cluster-proxy, managed-serviceaccount, and ClusterProfile support.

Enable ClusterProfile credential synchronization in the managed-serviceaccount add-on:

helm upgrade managed-serviceaccount ocm/managed-serviceaccount \
--kube-context kind-hub \
-n open-cluster-management-managed-serviceaccount \
--version 0.10.0 \
--reuse-values \
--set featureGates.clusterProfileCredSyncer=true

Bind the operator namespace to the ManagedClusterSet and label both ManagedServiceAccounts for credential synchronization:

apiVersion: cluster.open-cluster-management.io/v1beta2kind: ManagedClusterSetBindingmetadata:
name: sandbox-fleetnamespace: knative-operatorspec:
clusterSet: sandbox-fleet
kubectl --context kind-hub -n cluster1 label managedserviceaccount knative-operator \
authentication.open-cluster-management.io/sync-to-clusterprofile=true
kubectl --context kind-hub -n cluster2 label managedserviceaccount knative-operator \
authentication.open-cluster-management.io/sync-to-clusterprofile=true

Both ClusterProfiles reported ControlPlaneHealthy=True and Joined=True. The managed-serviceaccount add-on synchronized their credentials into the knative-operator namespace.

2. Configure the operator to use cp-creds

The operator mounted quay.io/open-cluster-management/cp-creds:latest as an image volume at /access-plugins/cp-creds and used this provider configuration:

{
"providers": [
{
"name": "open-cluster-management",
"execConfig": {
"apiVersion": "client.authentication.k8s.io/v1",
"command": "/access-plugins/cp-creds/cp-creds",
"args": ["--managed-serviceaccount=knative-operator"],
"provideClusterInfo": true,
"interactiveMode": "Never"
}
}
]
}

3. Build and deploy this PR head

PATH="$(go env GOPATH)/bin:$PATH" \
KO_DOCKER_REPO=kind.local \
KIND_CLUSTER_NAME=hub \
ko apply -f config/
kubectl --context kind-hub -n knative-operator \
rollout status deployment/knative-operator --timeout=180s

Both destination ClusterProfiles resolved through the configured cp-creds provider.

4. Configure the Serving data plane on cluster1

Gateway API v1.4.1 experimental CRDs and Envoy Gateway v1.7.1 were installed on cluster1 using the same GatewayNamespace deployment model as the original multicluster PR. Both GatewayClasses reported Accepted=True, and both Gateways reported Programmed=True.

5. Deploy Serving and Eventing to explicit remote namespaces

The management CR namespaces intentionally differ from the remote installation namespaces:

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: ocm-servingnamespace: management-serving-hubspec:
destination:
clusterProfileRef:
name: cluster1namespace: knative-operatornamespace: knative-servingnamespace:
labels:
operator.knative.dev/ocm-e2e: cluster1annotations:
operator.knative.dev/ocm-e2e-source: destinationingress:
gateway-api:
enabled: trueistio:
enabled: false
---
apiVersion: operator.knative.dev/v1beta1kind: KnativeEventingmetadata:
name: ocm-eventingnamespace: management-eventing-hubspec:
destination:
clusterProfileRef:
name: cluster2namespace: knative-operatornamespace: knative-eventingnamespace:
labels:
operator.knative.dev/ocm-e2e: cluster2annotations:
operator.knative.dev/ocm-e2e-source: destination

Both hub CRs reached TargetClusterResolved=True and Ready=True. All seven Serving Deployments became Available only in cluster1/knative-serving; Knative Eventing reached Ready=True with its controller, webhook, and broker Deployments only in cluster2/knative-eventing; no Knative Deployments appeared in either hub management namespace or on the wrong spoke. The remote namespace metadata was applied, and every managed Deployment was owned by its remote anchor ConfigMap.

6. Verify Serving traffic

kubectl --context kind-cluster1 apply -f - <<'EOF'apiVersion: serving.knative.dev/v1kind: Servicemetadata: name: helloworld-go namespace: defaultspec: template: spec: containers: - image: gcr.io/knative-samples/helloworld-go env: - name: TARGET value: OCM Native cp-creds E2EEOF
kubectl --context kind-cluster1 -n default \
wait --for=condition=Ready kservice/helloworld-go --timeout=300s
LB_IP=$(kubectl --context kind-cluster1 -n eg-external \ get service knative-external \ -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -H 'Host: helloworld-go.default.example.com'"http://${LB_IP}"

Observed response:

Hello OCM Native cp-creds E2E!

The external and cluster-local HTTPRoutes both reported Accepted=True and ResolvedRefs=True.

7. Verify cleanup with a cold cache

To exercise finalization before the ClusterProfile cache was warm, the operator was restarted and both management CRs were deleted immediately. Both remote anchors and their owned Deployments were removed, and both CRs completed deletion.

After recreating the CRs in the same destination namespaces, both returned to TargetClusterResolved=True and Ready=True.

Breaking change

Existing manifests using the unreleased top-level field must be rewritten:

# Beforespec:
clusterProfileRef:
name: cluster1namespace: cluster-inventory# Afterspec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

Release Note

action required: Replace spec.clusterProfileRef with spec.destination.clusterProfileRef and set spec.destination.namespace for remote Knative installations.

@knative-prowknative-prowBot added the size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. label Aug 22, 2026

@knative-prowknative-prowBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kahirokunn: 0 warnings.

Details

In response to this:

Proposed Changes

  • Replace the top-level spec.clusterProfileRef field with an explicit
    spec.destination object containing both clusterProfileRef and the remote
    installation namespace.
  • Reconcile remote manifests, ingress resources, and the anchor ConfigMap into
    spec.destination.namespace instead of coupling the remote installation to
    the management CR namespace.
  • Make the complete destination immutable and validate the ClusterProfile name,
    ClusterProfile namespace, and installation namespace as Kubernetes names.
  • Update generated CRDs, Helm CRDs, documentation, unit tests, and multicluster
    E2E coverage for the new API shape.

This is an intentional breaking replacement of an unreleased API with no
current consumers. No compatibility or migration path for the removed
top-level spec.clusterProfileRef field is retained.

Design

The existing multicluster reconciliation design remains unchanged: the
operator resolves the referenced Cluster Inventory API ClusterProfile, swaps
the manifest client at the start of reconciliation, and uses an anchor
ConfigMap for namespace-scoped garbage collection on the remote cluster.

This PR makes the destination explicit:

spec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

The management CR namespace and remote installation namespace are independent.
spec.namespace.labels and spec.namespace.annotations are merged into the
selected remote namespace while preserving unrelated existing metadata.

The complete spec.destination is immutable. Moving an existing installation
between clusters or namespaces requires deleting and recreating the management
CR so that remote finalization runs before the new destination is installed.

Example CR

KnativeServing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-serving-apacnamespace: management-servingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-servingingress:
gateway-api:
enabled: trueistio:
enabled: false

KnativeEventing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeEventingmetadata:
name: knative-eventing-apacnamespace: management-eventingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-eventing

Local cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-servingnamespace: knative-servingspec: {}# No destination: deploys to the local cluster as before.

Verification Steps

Automated verification

go test ./...
go test -run '^$' -tags='e2e multicluster' ./test/e2e
./hack/verify-codegen.sh

The repository multicluster Kind suite also passed all 11 tests:

SPOKE_CLUSTER_NAME=codex-destination-spoke \
./test/e2e-tests-multicluster.sh --kubeconfig=<hub-kubeconfig>

E2E verification with Kind

The following end-to-end flow was executed against this PR head. It uses three
Kind clusters (hub, cluster1, and cluster2), real OCM-managed
ClusterProfile objects, the official Cluster Inventory API secretreader
plugin, Envoy Gateway, and cloud-provider-kind.

1. Cluster and OCM setup

The OCM and Cluster Inventory API environment was created following the
OCM ClusterProfile guide:

kind create cluster --name hub
kind create cluster --name cluster1
kind create cluster --name cluster2
# Install OCM on hub, join cluster1 and cluster2, and enable ClusterProfile.# Install sandbox-fleet, cluster-proxy, and managed-serviceaccount.
kubectl --context kind-hub get managedclusters
kubectl --context kind-hub -n cluster-inventory get clusterprofiles
kubectl --context kind-hub get managedclusteraddons -A

Verified state:

  • cluster1 and cluster2: Joined=True, Available=True
  • generated ClusterProfiles: ControlPlaneHealthy=True, Joined=True
  • cluster-proxy and managed-serviceaccount: Available=True

2. Spoke credentials and ClusterProfile access

An OCM ManagedServiceAccount named knative-operator was created for each
managed cluster. The generated ClusterProfiles expose the cluster-proxy access
provider:

kubectl --context kind-hub -n cluster1 get managedserviceaccount knative-operator
kubectl --context kind-hub -n cluster-inventory get clusterprofile cluster1 -o yaml

The operator uses the official secretreader plugin:

{
"providers": [
{
"name": "open-cluster-management",
"execConfig": {
"apiVersion": "client.authentication.k8s.io/v1",
"command": "/access-plugins/secretreader/bin/secretreader-plugin",
"provideClusterInfo": true,
"interactiveMode": "Never"
}
}
]
}

The plugin image was
registry.k8s.io/cluster-inventory-api/secretreader:v0.1.3.

3. Operator deployment

PATH="$(go env GOPATH)/bin:$PATH" \
KO_DOCKER_REPO=kind.local \
KIND_CLUSTER_NAME=hub \
ko apply -f config/
kubectl --context kind-hub -n knative-operator \
rollout status deployment/knative-operator --timeout=180s

The operator was configured with:

--clusterprofile-provider-file=/etc/cluster-inventory/config.json
--remote-deployments-poll-interval=2s

The running operator reported commit 418c364 and successfully resolved the
OCM cluster-proxy endpoint for cluster-inventory/cluster1.

4. Install Envoy Gateway on cluster1

Gateway API v1.4.1 experimental CRDs were installed on the spoke. Envoy Gateway
v1.7.1 was then installed using the same GatewayNamespace deployment model as
the original multicluster PR:

cat <<'EOF' > /tmp/values-eg.yamlconfig: envoyGateway: provider: type: Kubernetes kubernetes: deploy: type: GatewayNamespaceEOF
kubectl --context kind-cluster1 create namespace envoy-gateway-system
helm template eg oci://docker.io/envoyproxy/gateway-helm \
--version v1.7.1 \
-n envoy-gateway-system \
-f /tmp/values-eg.yaml \
--include-crds | \
sed -n '/^---$/,$p'| \
kubectl --context kind-cluster1 apply --server-side --force-conflicts -f -
sudo cloud-provider-kind

5. Create external and internal Gateway resources

Two Envoy Gateway instances were created:

  • eg-external/eg-external: LoadBalancer, ports 80 and 443
  • eg-internal/eg-internal: ClusterIP, port 80
apiVersion: v1kind: Namespacemetadata:
name: eg-external
---
apiVersion: gateway.envoyproxy.io/v1alpha1kind: EnvoyProxymetadata:
name: knative-external-confignamespace: envoy-gateway-systemspec:
provider:
type: Kuberneteskubernetes:
envoyService:
name: knative-external
---
apiVersion: gateway.networking.k8s.io/v1kind: GatewayClassmetadata:
name: eg-externalspec:
controllerName: gateway.envoyproxy.io/gatewayclass-controllerparametersRef:
group: gateway.envoyproxy.iokind: EnvoyProxyname: knative-external-confignamespace: envoy-gateway-system
---
apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata:
name: eg-externalnamespace: eg-externalspec:
gatewayClassName: eg-externallisteners:
- name: httpport: 80protocol: HTTPallowedRoutes:
namespaces:
from: All
- name: tlsport: 443protocol: TLStls:
mode: PassthroughallowedRoutes:
namespaces:
from: All
---
apiVersion: v1kind: Namespacemetadata:
name: eg-internal
---
apiVersion: gateway.envoyproxy.io/v1alpha1kind: EnvoyProxymetadata:
name: knative-internal-confignamespace: envoy-gateway-systemspec:
provider:
type: Kuberneteskubernetes:
envoyService:
type: ClusterIPname: knative-internal
---
apiVersion: gateway.networking.k8s.io/v1kind: GatewayClassmetadata:
name: eg-internalspec:
controllerName: gateway.envoyproxy.io/gatewayclass-controllerparametersRef:
group: gateway.envoyproxy.iokind: EnvoyProxyname: knative-internal-confignamespace: envoy-gateway-system
---
apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata:
name: eg-internalnamespace: eg-internalspec:
gatewayClassName: eg-internallisteners:
- name: httpport: 80protocol: HTTPallowedRoutes:
namespaces:
from: All

Both GatewayClasses reached Accepted=True; both Gateways reached
Programmed=True. The external service received 172.18.0.12 from
cloud-provider-kind.

6. Deploy KnativeServing with an explicit destination

kubectl --context kind-hub apply -f - <<'EOF'apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata: name: ocm-serving namespace: management-serving-hubspec: destination: clusterProfileRef: name: cluster1 namespace: cluster-inventory namespace: knative-serving-ocm ingress: gateway-api: enabled: true istio: enabled: false config: config-gateway: external-gateways: | - class: eg-external gateway: eg-external/eg-external service: eg-external/knative-external supported-features: - HTTPRouteRequestTimeout local-gateways: | - class: eg-internal gateway: eg-internal/eg-internal service: eg-internal/knative-internal supported-features: - HTTPRouteRequestTimeout network: ingress-class: gateway-api.ingress.networking.knative.dev domain: example.com: ""EOF

The hub CR reached Ready=True and TargetClusterResolved=True. All seven
Knative Serving Deployments became Available in
cluster1/knative-serving-ocm, with no Serving Deployment in the hub management
namespace or on cluster2.

7. Verify ownership and data-plane traffic

The remote anchor was present and protected:

kubectl --context kind-cluster1 -n knative-serving-ocm \
get configmap knativeserving-ocm-serving-root-owner -o yaml
kubectl --context kind-cluster1 -n knative-serving-ocm \
get deployment activator -o jsonpath='{.metadata.ownerReferences}'| jq .

The activator Deployment ownerReference matched the anchor ConfigMap UID.

A Knative Service was then deployed on cluster1:

kubectl --context kind-cluster1 apply -f - <<'EOF'apiVersion: serving.knative.dev/v1kind: Servicemetadata: name: helloworld-go namespace: defaultspec: template: spec: containers: - image: gcr.io/knative-samples/helloworld-go env: - name: TARGET value: Destination Kind E2EEOF
kubectl --context kind-cluster1 -n default \
wait --for=condition=Ready kservice/helloworld-go --timeout=300s
LB_IP=$(kubectl --context kind-cluster1 -n eg-external \ get service knative-external \ -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -H 'Host: helloworld-go.default.example.com'"http://${LB_IP}"

Observed response:

Hello Destination Kind E2E!

The external and cluster-local HTTPRoutes both reported Accepted=True.

8. Verify CR deletion and finalizer cleanup

kubectl --context kind-cluster1 -n default delete kservice helloworld-go
kubectl --context kind-hub -n management-serving-hub \
delete knativeserving ocm-serving

Verified after finalization:

  • the hub KnativeServing CR was deleted
  • the remote anchor ConfigMap was deleted
  • all labeled Knative Serving Deployments were deleted
  • all Knative Serving ClusterRoles were deleted

The dynamically created autoscaler-bucket-00-of-01 Service and leader-election
Leases do not carry the anchor ownerReference and can remain after uninstall.
This also occurs on fork/main and is not introduced by the destination API
replacement. Those test-only remnants were removed before reinstalling into the
same namespace.

9. Recreate and leave the environment healthy

The same KnativeServing CR was recreated after cleanup. It returned to
Ready=True and TargetClusterResolved=True, and all seven Serving Deployments
became Available again. The OCM hub, both managed clusters, Envoy Gateway,
Knative Serving, and Knative Eventing were left running for inspection.

Destination validation

The real API server rejected all of the following:

  • invalid destination namespace
  • missing ClusterProfile name, ClusterProfile namespace, or target namespace
  • legacy top-level spec.clusterProfileRef
  • changes to destination.namespace
  • changes to destination.clusterProfileRef.name
  • changes to destination.clusterProfileRef.namespace
  • removal of spec.destination

Intentional breaking change

Existing manifests using the unreleased top-level field must be rewritten:

# Beforespec:
clusterProfileRef:
name: cluster1namespace: cluster-inventory# Afterspec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

No automatic migration or compatibility field is provided because the old API
has no current consumers.

Release Note

action required: Replace spec.clusterProfileRef with spec.destination.clusterProfileRef and set spec.destination.namespace for remote Knative installations.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@knative-prow

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: kahirokunn
Once this PR has been reviewed and has the lgtm label, please assign dsimansk for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@codecov

codecovBot commented Aug 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.14286% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.54%. Comparing base (0fc378e) to head (a5ca744).
⚠️ Report is 2 commits behind head on main.

Files with missing linesPatch %Lines
pkg/apis/operator/base/common.go0.00%2 Missing ⚠️
pkg/reconciler/common/multicluster.go94.73%1 Missing ⚠️
pkg/reconciler/knativeeventing/controller.go0.00%1 Missing ⚠️
pkg/reconciler/knativeserving/controller.go0.00%1 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #2344 +/- ##
==========================================
+ Coverage 63.99% 64.54% +0.55% 
==========================================
Files 55 55 Lines 2491 2502 +11 ==========================================
+ Hits 1594 1615 +21 + Misses 777 761 -16 - Partials 120 126 +6 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@kahirokunn

Copy link
Copy Markdown
MemberAuthor

/retest

@kahirokunn
kahirokunn marked this pull request as draft August 23, 2026 00:47
@knative-prowknative-prowBot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 23, 2026
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 418c364 to 449028eCompareAugust 23, 2026 00:54
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 449028e to 7345ad9CompareAugust 23, 2026 01:11
Signed-off-by: kahirokunn <okinakahiro@gmail.com>
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 7345ad9 to 8447d27CompareAugust 23, 2026 13:24
@kahirokunn
kahirokunn marked this pull request as ready for review August 23, 2026 13:28
@knative-prowknative-prowBot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 23, 2026
@knative-prow
knative-prowBot requested a review from matzewAugust 23, 2026 13:28
Signed-off-by: kahirokunn <okinakahiro@gmail.com>
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 8447d27 to a5ca744CompareAugust 23, 2026 13:29
@dsimansk

Copy link
Copy Markdown
Contributor

@kahirokunn is running multiple CRs of the same kind in parallel namespaces supported use case now? I.e. KnativeServing being on namespace: ns1, namespace: ns2 based on the same cluster profile?

@kahirokunn

Copy link
Copy Markdown
MemberAuthor

No, multiple CRs of the same kind targeting the same cluster are not intended to be supported.
I recalled the earlier discussion that each cluster supports only one Serving and one Eventing installation.
#1472 (comment)

Therefore, allowing an arbitrary destination namespace may be the wrong API design. The management CR may live in any hub namespace, while the remote installation namespace should probably be derived from the kind (knative-serving or knative-eventing). We should also prevent multiple CRs of the same kind from targeting the same ClusterProfile.

@kahirokunn
kahirokunn marked this pull request as draft August 26, 2026 02:16
@knative-prowknative-prowBot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 26, 2026
@kahirokunn

Copy link
Copy Markdown
MemberAuthor

Move to #2349

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge/work-in-progressIndicates that a PR should not merge because it is a work in progress.size/XLDenotes a PR that changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Install remote Knative components in canonical namespaces

2 participants

@kahirokunn@dsimansk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Support explicit remote namespaces via spec.destination - #2344

Closed
kahirokunn wants to merge 2 commits into
knative:mainfrom
kahirokunn:multicluster-destination
Closed

Support explicit remote namespaces via spec.destination#2344
kahirokunn wants to merge 2 commits into
knative:mainfrom
kahirokunn:multicluster-destination

Conversation

@kahirokunn

@kahirokunnkahirokunn commented Aug 22, 2026

Copy link
Copy Markdown
Member

Fixes#2345

Proposed Changes

  • Replace the top-level spec.clusterProfileRef field with an explicit spec.destination object containing both clusterProfileRef and the remote installation namespace.

Design

This PR makes the destination explicit:

spec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

The complete spec.destination is immutable. Moving an existing installation between clusters or namespaces requires deleting and recreating the management CR so that remote finalization runs before the new destination is installed.

Example CR

KnativeServing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-serving-apacnamespace: management-servingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-servingingress:
gateway-api:
enabled: trueistio:
enabled: false

KnativeEventing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeEventingmetadata:
name: knative-eventing-apacnamespace: management-eventingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-eventing

Local cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-servingnamespace: knative-servingspec: {}# No destination: deploys to the local cluster as before.

Verification Steps

E2E verification with Kind

Verified with three Kind clusters (hub, cluster1, and cluster2), OCM-managed ClusterProfile objects and cluster-proxy endpoints, the managed-serviceaccount ClusterProfileCredSyncer, cp-creds, Envoy Gateway, and cloud-provider-kind.

1. Set up OCM and Cluster Inventory API

The environment follows the OCM ClusterProfile guide: three Kind clusters, both spokes registered with OCM, the sandbox-fleet ManagedClusterSet, cluster-proxy, managed-serviceaccount, and ClusterProfile support.

Enable ClusterProfile credential synchronization in the managed-serviceaccount add-on:

helm upgrade managed-serviceaccount ocm/managed-serviceaccount \
--kube-context kind-hub \
-n open-cluster-management-managed-serviceaccount \
--version 0.10.0 \
--reuse-values \
--set featureGates.clusterProfileCredSyncer=true

Bind the operator namespace to the ManagedClusterSet and label both ManagedServiceAccounts for credential synchronization:

apiVersion: cluster.open-cluster-management.io/v1beta2kind: ManagedClusterSetBindingmetadata:
name: sandbox-fleetnamespace: knative-operatorspec:
clusterSet: sandbox-fleet
kubectl --context kind-hub -n cluster1 label managedserviceaccount knative-operator \
authentication.open-cluster-management.io/sync-to-clusterprofile=true
kubectl --context kind-hub -n cluster2 label managedserviceaccount knative-operator \
authentication.open-cluster-management.io/sync-to-clusterprofile=true

Both ClusterProfiles reported ControlPlaneHealthy=True and Joined=True. The managed-serviceaccount add-on synchronized their credentials into the knative-operator namespace.

2. Configure the operator to use cp-creds

The operator mounted quay.io/open-cluster-management/cp-creds:latest as an image volume at /access-plugins/cp-creds and used this provider configuration:

{
"providers": [
{
"name": "open-cluster-management",
"execConfig": {
"apiVersion": "client.authentication.k8s.io/v1",
"command": "/access-plugins/cp-creds/cp-creds",
"args": ["--managed-serviceaccount=knative-operator"],
"provideClusterInfo": true,
"interactiveMode": "Never"
}
}
]
}

3. Build and deploy this PR head

PATH="$(go env GOPATH)/bin:$PATH" \
KO_DOCKER_REPO=kind.local \
KIND_CLUSTER_NAME=hub \
ko apply -f config/
kubectl --context kind-hub -n knative-operator \
rollout status deployment/knative-operator --timeout=180s

Both destination ClusterProfiles resolved through the configured cp-creds provider.

4. Configure the Serving data plane on cluster1

Gateway API v1.4.1 experimental CRDs and Envoy Gateway v1.7.1 were installed on cluster1 using the same GatewayNamespace deployment model as the original multicluster PR. Both GatewayClasses reported Accepted=True, and both Gateways reported Programmed=True.

5. Deploy Serving and Eventing to explicit remote namespaces

The management CR namespaces intentionally differ from the remote installation namespaces:

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: ocm-servingnamespace: management-serving-hubspec:
destination:
clusterProfileRef:
name: cluster1namespace: knative-operatornamespace: knative-servingnamespace:
labels:
operator.knative.dev/ocm-e2e: cluster1annotations:
operator.knative.dev/ocm-e2e-source: destinationingress:
gateway-api:
enabled: trueistio:
enabled: false
---
apiVersion: operator.knative.dev/v1beta1kind: KnativeEventingmetadata:
name: ocm-eventingnamespace: management-eventing-hubspec:
destination:
clusterProfileRef:
name: cluster2namespace: knative-operatornamespace: knative-eventingnamespace:
labels:
operator.knative.dev/ocm-e2e: cluster2annotations:
operator.knative.dev/ocm-e2e-source: destination

Both hub CRs reached TargetClusterResolved=True and Ready=True. All seven Serving Deployments became Available only in cluster1/knative-serving; Knative Eventing reached Ready=True with its controller, webhook, and broker Deployments only in cluster2/knative-eventing; no Knative Deployments appeared in either hub management namespace or on the wrong spoke. The remote namespace metadata was applied, and every managed Deployment was owned by its remote anchor ConfigMap.

6. Verify Serving traffic

kubectl --context kind-cluster1 apply -f - <<'EOF'apiVersion: serving.knative.dev/v1kind: Servicemetadata: name: helloworld-go namespace: defaultspec: template: spec: containers: - image: gcr.io/knative-samples/helloworld-go env: - name: TARGET value: OCM Native cp-creds E2EEOF
kubectl --context kind-cluster1 -n default \
wait --for=condition=Ready kservice/helloworld-go --timeout=300s
LB_IP=$(kubectl --context kind-cluster1 -n eg-external \ get service knative-external \ -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -H 'Host: helloworld-go.default.example.com'"http://${LB_IP}"

Observed response:

Hello OCM Native cp-creds E2E!

The external and cluster-local HTTPRoutes both reported Accepted=True and ResolvedRefs=True.

7. Verify cleanup with a cold cache

To exercise finalization before the ClusterProfile cache was warm, the operator was restarted and both management CRs were deleted immediately. Both remote anchors and their owned Deployments were removed, and both CRs completed deletion.

After recreating the CRs in the same destination namespaces, both returned to TargetClusterResolved=True and Ready=True.

Breaking change

Existing manifests using the unreleased top-level field must be rewritten:

# Beforespec:
clusterProfileRef:
name: cluster1namespace: cluster-inventory# Afterspec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

Release Note

action required: Replace spec.clusterProfileRef with spec.destination.clusterProfileRef and set spec.destination.namespace for remote Knative installations.

@knative-prowknative-prowBot added the size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. label Aug 22, 2026

@knative-prowknative-prowBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kahirokunn: 0 warnings.

Details

In response to this:

Proposed Changes

  • Replace the top-level spec.clusterProfileRef field with an explicit
    spec.destination object containing both clusterProfileRef and the remote
    installation namespace.
  • Reconcile remote manifests, ingress resources, and the anchor ConfigMap into
    spec.destination.namespace instead of coupling the remote installation to
    the management CR namespace.
  • Make the complete destination immutable and validate the ClusterProfile name,
    ClusterProfile namespace, and installation namespace as Kubernetes names.
  • Update generated CRDs, Helm CRDs, documentation, unit tests, and multicluster
    E2E coverage for the new API shape.

This is an intentional breaking replacement of an unreleased API with no
current consumers. No compatibility or migration path for the removed
top-level spec.clusterProfileRef field is retained.

Design

The existing multicluster reconciliation design remains unchanged: the
operator resolves the referenced Cluster Inventory API ClusterProfile, swaps
the manifest client at the start of reconciliation, and uses an anchor
ConfigMap for namespace-scoped garbage collection on the remote cluster.

This PR makes the destination explicit:

spec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

The management CR namespace and remote installation namespace are independent.
spec.namespace.labels and spec.namespace.annotations are merged into the
selected remote namespace while preserving unrelated existing metadata.

The complete spec.destination is immutable. Moving an existing installation
between clusters or namespaces requires deleting and recreating the management
CR so that remote finalization runs before the new destination is installed.

Example CR

KnativeServing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-serving-apacnamespace: management-servingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-servingingress:
gateway-api:
enabled: trueistio:
enabled: false

KnativeEventing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeEventingmetadata:
name: knative-eventing-apacnamespace: management-eventingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-eventing

Local cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-servingnamespace: knative-servingspec: {}# No destination: deploys to the local cluster as before.

Verification Steps

Automated verification

go test ./...
go test -run '^$' -tags='e2e multicluster' ./test/e2e
./hack/verify-codegen.sh

The repository multicluster Kind suite also passed all 11 tests:

SPOKE_CLUSTER_NAME=codex-destination-spoke \
./test/e2e-tests-multicluster.sh --kubeconfig=<hub-kubeconfig>

E2E verification with Kind

The following end-to-end flow was executed against this PR head. It uses three
Kind clusters (hub, cluster1, and cluster2), real OCM-managed
ClusterProfile objects, the official Cluster Inventory API secretreader
plugin, Envoy Gateway, and cloud-provider-kind.

1. Cluster and OCM setup

The OCM and Cluster Inventory API environment was created following the
OCM ClusterProfile guide:

kind create cluster --name hub
kind create cluster --name cluster1
kind create cluster --name cluster2
# Install OCM on hub, join cluster1 and cluster2, and enable ClusterProfile.# Install sandbox-fleet, cluster-proxy, and managed-serviceaccount.
kubectl --context kind-hub get managedclusters
kubectl --context kind-hub -n cluster-inventory get clusterprofiles
kubectl --context kind-hub get managedclusteraddons -A

Verified state:

  • cluster1 and cluster2: Joined=True, Available=True
  • generated ClusterProfiles: ControlPlaneHealthy=True, Joined=True
  • cluster-proxy and managed-serviceaccount: Available=True

2. Spoke credentials and ClusterProfile access

An OCM ManagedServiceAccount named knative-operator was created for each
managed cluster. The generated ClusterProfiles expose the cluster-proxy access
provider:

kubectl --context kind-hub -n cluster1 get managedserviceaccount knative-operator
kubectl --context kind-hub -n cluster-inventory get clusterprofile cluster1 -o yaml

The operator uses the official secretreader plugin:

{
"providers": [
{
"name": "open-cluster-management",
"execConfig": {
"apiVersion": "client.authentication.k8s.io/v1",
"command": "/access-plugins/secretreader/bin/secretreader-plugin",
"provideClusterInfo": true,
"interactiveMode": "Never"
}
}
]
}

The plugin image was
registry.k8s.io/cluster-inventory-api/secretreader:v0.1.3.

3. Operator deployment

PATH="$(go env GOPATH)/bin:$PATH" \
KO_DOCKER_REPO=kind.local \
KIND_CLUSTER_NAME=hub \
ko apply -f config/
kubectl --context kind-hub -n knative-operator \
rollout status deployment/knative-operator --timeout=180s

The operator was configured with:

--clusterprofile-provider-file=/etc/cluster-inventory/config.json
--remote-deployments-poll-interval=2s

The running operator reported commit 418c364 and successfully resolved the
OCM cluster-proxy endpoint for cluster-inventory/cluster1.

4. Install Envoy Gateway on cluster1

Gateway API v1.4.1 experimental CRDs were installed on the spoke. Envoy Gateway
v1.7.1 was then installed using the same GatewayNamespace deployment model as
the original multicluster PR:

cat <<'EOF' > /tmp/values-eg.yamlconfig: envoyGateway: provider: type: Kubernetes kubernetes: deploy: type: GatewayNamespaceEOF
kubectl --context kind-cluster1 create namespace envoy-gateway-system
helm template eg oci://docker.io/envoyproxy/gateway-helm \
--version v1.7.1 \
-n envoy-gateway-system \
-f /tmp/values-eg.yaml \
--include-crds | \
sed -n '/^---$/,$p'| \
kubectl --context kind-cluster1 apply --server-side --force-conflicts -f -
sudo cloud-provider-kind

5. Create external and internal Gateway resources

Two Envoy Gateway instances were created:

  • eg-external/eg-external: LoadBalancer, ports 80 and 443
  • eg-internal/eg-internal: ClusterIP, port 80
apiVersion: v1kind: Namespacemetadata:
name: eg-external
---
apiVersion: gateway.envoyproxy.io/v1alpha1kind: EnvoyProxymetadata:
name: knative-external-confignamespace: envoy-gateway-systemspec:
provider:
type: Kuberneteskubernetes:
envoyService:
name: knative-external
---
apiVersion: gateway.networking.k8s.io/v1kind: GatewayClassmetadata:
name: eg-externalspec:
controllerName: gateway.envoyproxy.io/gatewayclass-controllerparametersRef:
group: gateway.envoyproxy.iokind: EnvoyProxyname: knative-external-confignamespace: envoy-gateway-system
---
apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata:
name: eg-externalnamespace: eg-externalspec:
gatewayClassName: eg-externallisteners:
- name: httpport: 80protocol: HTTPallowedRoutes:
namespaces:
from: All
- name: tlsport: 443protocol: TLStls:
mode: PassthroughallowedRoutes:
namespaces:
from: All
---
apiVersion: v1kind: Namespacemetadata:
name: eg-internal
---
apiVersion: gateway.envoyproxy.io/v1alpha1kind: EnvoyProxymetadata:
name: knative-internal-confignamespace: envoy-gateway-systemspec:
provider:
type: Kuberneteskubernetes:
envoyService:
type: ClusterIPname: knative-internal
---
apiVersion: gateway.networking.k8s.io/v1kind: GatewayClassmetadata:
name: eg-internalspec:
controllerName: gateway.envoyproxy.io/gatewayclass-controllerparametersRef:
group: gateway.envoyproxy.iokind: EnvoyProxyname: knative-internal-confignamespace: envoy-gateway-system
---
apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata:
name: eg-internalnamespace: eg-internalspec:
gatewayClassName: eg-internallisteners:
- name: httpport: 80protocol: HTTPallowedRoutes:
namespaces:
from: All

Both GatewayClasses reached Accepted=True; both Gateways reached
Programmed=True. The external service received 172.18.0.12 from
cloud-provider-kind.

6. Deploy KnativeServing with an explicit destination

kubectl --context kind-hub apply -f - <<'EOF'apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata: name: ocm-serving namespace: management-serving-hubspec: destination: clusterProfileRef: name: cluster1 namespace: cluster-inventory namespace: knative-serving-ocm ingress: gateway-api: enabled: true istio: enabled: false config: config-gateway: external-gateways: | - class: eg-external gateway: eg-external/eg-external service: eg-external/knative-external supported-features: - HTTPRouteRequestTimeout local-gateways: | - class: eg-internal gateway: eg-internal/eg-internal service: eg-internal/knative-internal supported-features: - HTTPRouteRequestTimeout network: ingress-class: gateway-api.ingress.networking.knative.dev domain: example.com: ""EOF

The hub CR reached Ready=True and TargetClusterResolved=True. All seven
Knative Serving Deployments became Available in
cluster1/knative-serving-ocm, with no Serving Deployment in the hub management
namespace or on cluster2.

7. Verify ownership and data-plane traffic

The remote anchor was present and protected:

kubectl --context kind-cluster1 -n knative-serving-ocm \
get configmap knativeserving-ocm-serving-root-owner -o yaml
kubectl --context kind-cluster1 -n knative-serving-ocm \
get deployment activator -o jsonpath='{.metadata.ownerReferences}'| jq .

The activator Deployment ownerReference matched the anchor ConfigMap UID.

A Knative Service was then deployed on cluster1:

kubectl --context kind-cluster1 apply -f - <<'EOF'apiVersion: serving.knative.dev/v1kind: Servicemetadata: name: helloworld-go namespace: defaultspec: template: spec: containers: - image: gcr.io/knative-samples/helloworld-go env: - name: TARGET value: Destination Kind E2EEOF
kubectl --context kind-cluster1 -n default \
wait --for=condition=Ready kservice/helloworld-go --timeout=300s
LB_IP=$(kubectl --context kind-cluster1 -n eg-external \ get service knative-external \ -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -H 'Host: helloworld-go.default.example.com'"http://${LB_IP}"

Observed response:

Hello Destination Kind E2E!

The external and cluster-local HTTPRoutes both reported Accepted=True.

8. Verify CR deletion and finalizer cleanup

kubectl --context kind-cluster1 -n default delete kservice helloworld-go
kubectl --context kind-hub -n management-serving-hub \
delete knativeserving ocm-serving

Verified after finalization:

  • the hub KnativeServing CR was deleted
  • the remote anchor ConfigMap was deleted
  • all labeled Knative Serving Deployments were deleted
  • all Knative Serving ClusterRoles were deleted

The dynamically created autoscaler-bucket-00-of-01 Service and leader-election
Leases do not carry the anchor ownerReference and can remain after uninstall.
This also occurs on fork/main and is not introduced by the destination API
replacement. Those test-only remnants were removed before reinstalling into the
same namespace.

9. Recreate and leave the environment healthy

The same KnativeServing CR was recreated after cleanup. It returned to
Ready=True and TargetClusterResolved=True, and all seven Serving Deployments
became Available again. The OCM hub, both managed clusters, Envoy Gateway,
Knative Serving, and Knative Eventing were left running for inspection.

Destination validation

The real API server rejected all of the following:

  • invalid destination namespace
  • missing ClusterProfile name, ClusterProfile namespace, or target namespace
  • legacy top-level spec.clusterProfileRef
  • changes to destination.namespace
  • changes to destination.clusterProfileRef.name
  • changes to destination.clusterProfileRef.namespace
  • removal of spec.destination

Intentional breaking change

Existing manifests using the unreleased top-level field must be rewritten:

# Beforespec:
clusterProfileRef:
name: cluster1namespace: cluster-inventory# Afterspec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

No automatic migration or compatibility field is provided because the old API
has no current consumers.

Release Note

action required: Replace spec.clusterProfileRef with spec.destination.clusterProfileRef and set spec.destination.namespace for remote Knative installations.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@knative-prow

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: kahirokunn
Once this PR has been reviewed and has the lgtm label, please assign dsimansk for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@codecov

codecovBot commented Aug 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.14286% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.54%. Comparing base (0fc378e) to head (a5ca744).
⚠️ Report is 2 commits behind head on main.

Files with missing linesPatch %Lines
pkg/apis/operator/base/common.go0.00%2 Missing ⚠️
pkg/reconciler/common/multicluster.go94.73%1 Missing ⚠️
pkg/reconciler/knativeeventing/controller.go0.00%1 Missing ⚠️
pkg/reconciler/knativeserving/controller.go0.00%1 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #2344 +/- ##
==========================================
+ Coverage 63.99% 64.54% +0.55% 
==========================================
Files 55 55 Lines 2491 2502 +11 ==========================================
+ Hits 1594 1615 +21 + Misses 777 761 -16 - Partials 120 126 +6 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@kahirokunn

Copy link
Copy Markdown
MemberAuthor

/retest

@kahirokunn
kahirokunn marked this pull request as draft August 23, 2026 00:47
@knative-prowknative-prowBot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 23, 2026
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 418c364 to 449028eCompareAugust 23, 2026 00:54
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 449028e to 7345ad9CompareAugust 23, 2026 01:11
Signed-off-by: kahirokunn <okinakahiro@gmail.com>
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 7345ad9 to 8447d27CompareAugust 23, 2026 13:24
@kahirokunn
kahirokunn marked this pull request as ready for review August 23, 2026 13:28
@knative-prowknative-prowBot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 23, 2026
@knative-prow
knative-prowBot requested a review from matzewAugust 23, 2026 13:28
Signed-off-by: kahirokunn <okinakahiro@gmail.com>
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 8447d27 to a5ca744CompareAugust 23, 2026 13:29
@dsimansk

Copy link
Copy Markdown
Contributor

@kahirokunn is running multiple CRs of the same kind in parallel namespaces supported use case now? I.e. KnativeServing being on namespace: ns1, namespace: ns2 based on the same cluster profile?

@kahirokunn

Copy link
Copy Markdown
MemberAuthor

No, multiple CRs of the same kind targeting the same cluster are not intended to be supported.
I recalled the earlier discussion that each cluster supports only one Serving and one Eventing installation.
#1472 (comment)

Therefore, allowing an arbitrary destination namespace may be the wrong API design. The management CR may live in any hub namespace, while the remote installation namespace should probably be derived from the kind (knative-serving or knative-eventing). We should also prevent multiple CRs of the same kind from targeting the same ClusterProfile.

@kahirokunn
kahirokunn marked this pull request as draft August 26, 2026 02:16
@knative-prowknative-prowBot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 26, 2026
@kahirokunn

Copy link
Copy Markdown
MemberAuthor

Move to #2349

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge/work-in-progressIndicates that a PR should not merge because it is a work in progress.size/XLDenotes a PR that changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Install remote Knative components in canonical namespaces

2 participants

@kahirokunn@dsimansk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Support explicit remote namespaces via spec.destination - #2344

Closed
kahirokunn wants to merge 2 commits into
knative:mainfrom
kahirokunn:multicluster-destination
Closed

Support explicit remote namespaces via spec.destination#2344
kahirokunn wants to merge 2 commits into
knative:mainfrom
kahirokunn:multicluster-destination

Conversation

@kahirokunn

@kahirokunnkahirokunn commented Aug 22, 2026

Copy link
Copy Markdown
Member

Fixes#2345

Proposed Changes

  • Replace the top-level spec.clusterProfileRef field with an explicit spec.destination object containing both clusterProfileRef and the remote installation namespace.

Design

This PR makes the destination explicit:

spec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

The complete spec.destination is immutable. Moving an existing installation between clusters or namespaces requires deleting and recreating the management CR so that remote finalization runs before the new destination is installed.

Example CR

KnativeServing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-serving-apacnamespace: management-servingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-servingingress:
gateway-api:
enabled: trueistio:
enabled: false

KnativeEventing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeEventingmetadata:
name: knative-eventing-apacnamespace: management-eventingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-eventing

Local cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-servingnamespace: knative-servingspec: {}# No destination: deploys to the local cluster as before.

Verification Steps

E2E verification with Kind

Verified with three Kind clusters (hub, cluster1, and cluster2), OCM-managed ClusterProfile objects and cluster-proxy endpoints, the managed-serviceaccount ClusterProfileCredSyncer, cp-creds, Envoy Gateway, and cloud-provider-kind.

1. Set up OCM and Cluster Inventory API

The environment follows the OCM ClusterProfile guide: three Kind clusters, both spokes registered with OCM, the sandbox-fleet ManagedClusterSet, cluster-proxy, managed-serviceaccount, and ClusterProfile support.

Enable ClusterProfile credential synchronization in the managed-serviceaccount add-on:

helm upgrade managed-serviceaccount ocm/managed-serviceaccount \
--kube-context kind-hub \
-n open-cluster-management-managed-serviceaccount \
--version 0.10.0 \
--reuse-values \
--set featureGates.clusterProfileCredSyncer=true

Bind the operator namespace to the ManagedClusterSet and label both ManagedServiceAccounts for credential synchronization:

apiVersion: cluster.open-cluster-management.io/v1beta2kind: ManagedClusterSetBindingmetadata:
name: sandbox-fleetnamespace: knative-operatorspec:
clusterSet: sandbox-fleet
kubectl --context kind-hub -n cluster1 label managedserviceaccount knative-operator \
authentication.open-cluster-management.io/sync-to-clusterprofile=true
kubectl --context kind-hub -n cluster2 label managedserviceaccount knative-operator \
authentication.open-cluster-management.io/sync-to-clusterprofile=true

Both ClusterProfiles reported ControlPlaneHealthy=True and Joined=True. The managed-serviceaccount add-on synchronized their credentials into the knative-operator namespace.

2. Configure the operator to use cp-creds

The operator mounted quay.io/open-cluster-management/cp-creds:latest as an image volume at /access-plugins/cp-creds and used this provider configuration:

{
"providers": [
{
"name": "open-cluster-management",
"execConfig": {
"apiVersion": "client.authentication.k8s.io/v1",
"command": "/access-plugins/cp-creds/cp-creds",
"args": ["--managed-serviceaccount=knative-operator"],
"provideClusterInfo": true,
"interactiveMode": "Never"
}
}
]
}

3. Build and deploy this PR head

PATH="$(go env GOPATH)/bin:$PATH" \
KO_DOCKER_REPO=kind.local \
KIND_CLUSTER_NAME=hub \
ko apply -f config/
kubectl --context kind-hub -n knative-operator \
rollout status deployment/knative-operator --timeout=180s

Both destination ClusterProfiles resolved through the configured cp-creds provider.

4. Configure the Serving data plane on cluster1

Gateway API v1.4.1 experimental CRDs and Envoy Gateway v1.7.1 were installed on cluster1 using the same GatewayNamespace deployment model as the original multicluster PR. Both GatewayClasses reported Accepted=True, and both Gateways reported Programmed=True.

5. Deploy Serving and Eventing to explicit remote namespaces

The management CR namespaces intentionally differ from the remote installation namespaces:

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: ocm-servingnamespace: management-serving-hubspec:
destination:
clusterProfileRef:
name: cluster1namespace: knative-operatornamespace: knative-servingnamespace:
labels:
operator.knative.dev/ocm-e2e: cluster1annotations:
operator.knative.dev/ocm-e2e-source: destinationingress:
gateway-api:
enabled: trueistio:
enabled: false
---
apiVersion: operator.knative.dev/v1beta1kind: KnativeEventingmetadata:
name: ocm-eventingnamespace: management-eventing-hubspec:
destination:
clusterProfileRef:
name: cluster2namespace: knative-operatornamespace: knative-eventingnamespace:
labels:
operator.knative.dev/ocm-e2e: cluster2annotations:
operator.knative.dev/ocm-e2e-source: destination

Both hub CRs reached TargetClusterResolved=True and Ready=True. All seven Serving Deployments became Available only in cluster1/knative-serving; Knative Eventing reached Ready=True with its controller, webhook, and broker Deployments only in cluster2/knative-eventing; no Knative Deployments appeared in either hub management namespace or on the wrong spoke. The remote namespace metadata was applied, and every managed Deployment was owned by its remote anchor ConfigMap.

6. Verify Serving traffic

kubectl --context kind-cluster1 apply -f - <<'EOF'apiVersion: serving.knative.dev/v1kind: Servicemetadata: name: helloworld-go namespace: defaultspec: template: spec: containers: - image: gcr.io/knative-samples/helloworld-go env: - name: TARGET value: OCM Native cp-creds E2EEOF
kubectl --context kind-cluster1 -n default \
wait --for=condition=Ready kservice/helloworld-go --timeout=300s
LB_IP=$(kubectl --context kind-cluster1 -n eg-external \ get service knative-external \ -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -H 'Host: helloworld-go.default.example.com'"http://${LB_IP}"

Observed response:

Hello OCM Native cp-creds E2E!

The external and cluster-local HTTPRoutes both reported Accepted=True and ResolvedRefs=True.

7. Verify cleanup with a cold cache

To exercise finalization before the ClusterProfile cache was warm, the operator was restarted and both management CRs were deleted immediately. Both remote anchors and their owned Deployments were removed, and both CRs completed deletion.

After recreating the CRs in the same destination namespaces, both returned to TargetClusterResolved=True and Ready=True.

Breaking change

Existing manifests using the unreleased top-level field must be rewritten:

# Beforespec:
clusterProfileRef:
name: cluster1namespace: cluster-inventory# Afterspec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

Release Note

action required: Replace spec.clusterProfileRef with spec.destination.clusterProfileRef and set spec.destination.namespace for remote Knative installations.

@knative-prowknative-prowBot added the size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. label Aug 22, 2026

@knative-prowknative-prowBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kahirokunn: 0 warnings.

Details

In response to this:

Proposed Changes

  • Replace the top-level spec.clusterProfileRef field with an explicit
    spec.destination object containing both clusterProfileRef and the remote
    installation namespace.
  • Reconcile remote manifests, ingress resources, and the anchor ConfigMap into
    spec.destination.namespace instead of coupling the remote installation to
    the management CR namespace.
  • Make the complete destination immutable and validate the ClusterProfile name,
    ClusterProfile namespace, and installation namespace as Kubernetes names.
  • Update generated CRDs, Helm CRDs, documentation, unit tests, and multicluster
    E2E coverage for the new API shape.

This is an intentional breaking replacement of an unreleased API with no
current consumers. No compatibility or migration path for the removed
top-level spec.clusterProfileRef field is retained.

Design

The existing multicluster reconciliation design remains unchanged: the
operator resolves the referenced Cluster Inventory API ClusterProfile, swaps
the manifest client at the start of reconciliation, and uses an anchor
ConfigMap for namespace-scoped garbage collection on the remote cluster.

This PR makes the destination explicit:

spec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

The management CR namespace and remote installation namespace are independent.
spec.namespace.labels and spec.namespace.annotations are merged into the
selected remote namespace while preserving unrelated existing metadata.

The complete spec.destination is immutable. Moving an existing installation
between clusters or namespaces requires deleting and recreating the management
CR so that remote finalization runs before the new destination is installed.

Example CR

KnativeServing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-serving-apacnamespace: management-servingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-servingingress:
gateway-api:
enabled: trueistio:
enabled: false

KnativeEventing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeEventingmetadata:
name: knative-eventing-apacnamespace: management-eventingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-eventing

Local cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-servingnamespace: knative-servingspec: {}# No destination: deploys to the local cluster as before.

Verification Steps

Automated verification

go test ./...
go test -run '^$' -tags='e2e multicluster' ./test/e2e
./hack/verify-codegen.sh

The repository multicluster Kind suite also passed all 11 tests:

SPOKE_CLUSTER_NAME=codex-destination-spoke \
./test/e2e-tests-multicluster.sh --kubeconfig=<hub-kubeconfig>

E2E verification with Kind

The following end-to-end flow was executed against this PR head. It uses three
Kind clusters (hub, cluster1, and cluster2), real OCM-managed
ClusterProfile objects, the official Cluster Inventory API secretreader
plugin, Envoy Gateway, and cloud-provider-kind.

1. Cluster and OCM setup

The OCM and Cluster Inventory API environment was created following the
OCM ClusterProfile guide:

kind create cluster --name hub
kind create cluster --name cluster1
kind create cluster --name cluster2
# Install OCM on hub, join cluster1 and cluster2, and enable ClusterProfile.# Install sandbox-fleet, cluster-proxy, and managed-serviceaccount.
kubectl --context kind-hub get managedclusters
kubectl --context kind-hub -n cluster-inventory get clusterprofiles
kubectl --context kind-hub get managedclusteraddons -A

Verified state:

  • cluster1 and cluster2: Joined=True, Available=True
  • generated ClusterProfiles: ControlPlaneHealthy=True, Joined=True
  • cluster-proxy and managed-serviceaccount: Available=True

2. Spoke credentials and ClusterProfile access

An OCM ManagedServiceAccount named knative-operator was created for each
managed cluster. The generated ClusterProfiles expose the cluster-proxy access
provider:

kubectl --context kind-hub -n cluster1 get managedserviceaccount knative-operator
kubectl --context kind-hub -n cluster-inventory get clusterprofile cluster1 -o yaml

The operator uses the official secretreader plugin:

{
"providers": [
{
"name": "open-cluster-management",
"execConfig": {
"apiVersion": "client.authentication.k8s.io/v1",
"command": "/access-plugins/secretreader/bin/secretreader-plugin",
"provideClusterInfo": true,
"interactiveMode": "Never"
}
}
]
}

The plugin image was
registry.k8s.io/cluster-inventory-api/secretreader:v0.1.3.

3. Operator deployment

PATH="$(go env GOPATH)/bin:$PATH" \
KO_DOCKER_REPO=kind.local \
KIND_CLUSTER_NAME=hub \
ko apply -f config/
kubectl --context kind-hub -n knative-operator \
rollout status deployment/knative-operator --timeout=180s

The operator was configured with:

--clusterprofile-provider-file=/etc/cluster-inventory/config.json
--remote-deployments-poll-interval=2s

The running operator reported commit 418c364 and successfully resolved the
OCM cluster-proxy endpoint for cluster-inventory/cluster1.

4. Install Envoy Gateway on cluster1

Gateway API v1.4.1 experimental CRDs were installed on the spoke. Envoy Gateway
v1.7.1 was then installed using the same GatewayNamespace deployment model as
the original multicluster PR:

cat <<'EOF' > /tmp/values-eg.yamlconfig: envoyGateway: provider: type: Kubernetes kubernetes: deploy: type: GatewayNamespaceEOF
kubectl --context kind-cluster1 create namespace envoy-gateway-system
helm template eg oci://docker.io/envoyproxy/gateway-helm \
--version v1.7.1 \
-n envoy-gateway-system \
-f /tmp/values-eg.yaml \
--include-crds | \
sed -n '/^---$/,$p'| \
kubectl --context kind-cluster1 apply --server-side --force-conflicts -f -
sudo cloud-provider-kind

5. Create external and internal Gateway resources

Two Envoy Gateway instances were created:

  • eg-external/eg-external: LoadBalancer, ports 80 and 443
  • eg-internal/eg-internal: ClusterIP, port 80
apiVersion: v1kind: Namespacemetadata:
name: eg-external
---
apiVersion: gateway.envoyproxy.io/v1alpha1kind: EnvoyProxymetadata:
name: knative-external-confignamespace: envoy-gateway-systemspec:
provider:
type: Kuberneteskubernetes:
envoyService:
name: knative-external
---
apiVersion: gateway.networking.k8s.io/v1kind: GatewayClassmetadata:
name: eg-externalspec:
controllerName: gateway.envoyproxy.io/gatewayclass-controllerparametersRef:
group: gateway.envoyproxy.iokind: EnvoyProxyname: knative-external-confignamespace: envoy-gateway-system
---
apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata:
name: eg-externalnamespace: eg-externalspec:
gatewayClassName: eg-externallisteners:
- name: httpport: 80protocol: HTTPallowedRoutes:
namespaces:
from: All
- name: tlsport: 443protocol: TLStls:
mode: PassthroughallowedRoutes:
namespaces:
from: All
---
apiVersion: v1kind: Namespacemetadata:
name: eg-internal
---
apiVersion: gateway.envoyproxy.io/v1alpha1kind: EnvoyProxymetadata:
name: knative-internal-confignamespace: envoy-gateway-systemspec:
provider:
type: Kuberneteskubernetes:
envoyService:
type: ClusterIPname: knative-internal
---
apiVersion: gateway.networking.k8s.io/v1kind: GatewayClassmetadata:
name: eg-internalspec:
controllerName: gateway.envoyproxy.io/gatewayclass-controllerparametersRef:
group: gateway.envoyproxy.iokind: EnvoyProxyname: knative-internal-confignamespace: envoy-gateway-system
---
apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata:
name: eg-internalnamespace: eg-internalspec:
gatewayClassName: eg-internallisteners:
- name: httpport: 80protocol: HTTPallowedRoutes:
namespaces:
from: All

Both GatewayClasses reached Accepted=True; both Gateways reached
Programmed=True. The external service received 172.18.0.12 from
cloud-provider-kind.

6. Deploy KnativeServing with an explicit destination

kubectl --context kind-hub apply -f - <<'EOF'apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata: name: ocm-serving namespace: management-serving-hubspec: destination: clusterProfileRef: name: cluster1 namespace: cluster-inventory namespace: knative-serving-ocm ingress: gateway-api: enabled: true istio: enabled: false config: config-gateway: external-gateways: | - class: eg-external gateway: eg-external/eg-external service: eg-external/knative-external supported-features: - HTTPRouteRequestTimeout local-gateways: | - class: eg-internal gateway: eg-internal/eg-internal service: eg-internal/knative-internal supported-features: - HTTPRouteRequestTimeout network: ingress-class: gateway-api.ingress.networking.knative.dev domain: example.com: ""EOF

The hub CR reached Ready=True and TargetClusterResolved=True. All seven
Knative Serving Deployments became Available in
cluster1/knative-serving-ocm, with no Serving Deployment in the hub management
namespace or on cluster2.

7. Verify ownership and data-plane traffic

The remote anchor was present and protected:

kubectl --context kind-cluster1 -n knative-serving-ocm \
get configmap knativeserving-ocm-serving-root-owner -o yaml
kubectl --context kind-cluster1 -n knative-serving-ocm \
get deployment activator -o jsonpath='{.metadata.ownerReferences}'| jq .

The activator Deployment ownerReference matched the anchor ConfigMap UID.

A Knative Service was then deployed on cluster1:

kubectl --context kind-cluster1 apply -f - <<'EOF'apiVersion: serving.knative.dev/v1kind: Servicemetadata: name: helloworld-go namespace: defaultspec: template: spec: containers: - image: gcr.io/knative-samples/helloworld-go env: - name: TARGET value: Destination Kind E2EEOF
kubectl --context kind-cluster1 -n default \
wait --for=condition=Ready kservice/helloworld-go --timeout=300s
LB_IP=$(kubectl --context kind-cluster1 -n eg-external \ get service knative-external \ -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -H 'Host: helloworld-go.default.example.com'"http://${LB_IP}"

Observed response:

Hello Destination Kind E2E!

The external and cluster-local HTTPRoutes both reported Accepted=True.

8. Verify CR deletion and finalizer cleanup

kubectl --context kind-cluster1 -n default delete kservice helloworld-go
kubectl --context kind-hub -n management-serving-hub \
delete knativeserving ocm-serving

Verified after finalization:

  • the hub KnativeServing CR was deleted
  • the remote anchor ConfigMap was deleted
  • all labeled Knative Serving Deployments were deleted
  • all Knative Serving ClusterRoles were deleted

The dynamically created autoscaler-bucket-00-of-01 Service and leader-election
Leases do not carry the anchor ownerReference and can remain after uninstall.
This also occurs on fork/main and is not introduced by the destination API
replacement. Those test-only remnants were removed before reinstalling into the
same namespace.

9. Recreate and leave the environment healthy

The same KnativeServing CR was recreated after cleanup. It returned to
Ready=True and TargetClusterResolved=True, and all seven Serving Deployments
became Available again. The OCM hub, both managed clusters, Envoy Gateway,
Knative Serving, and Knative Eventing were left running for inspection.

Destination validation

The real API server rejected all of the following:

  • invalid destination namespace
  • missing ClusterProfile name, ClusterProfile namespace, or target namespace
  • legacy top-level spec.clusterProfileRef
  • changes to destination.namespace
  • changes to destination.clusterProfileRef.name
  • changes to destination.clusterProfileRef.namespace
  • removal of spec.destination

Intentional breaking change

Existing manifests using the unreleased top-level field must be rewritten:

# Beforespec:
clusterProfileRef:
name: cluster1namespace: cluster-inventory# Afterspec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

No automatic migration or compatibility field is provided because the old API
has no current consumers.

Release Note

action required: Replace spec.clusterProfileRef with spec.destination.clusterProfileRef and set spec.destination.namespace for remote Knative installations.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@knative-prow

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: kahirokunn
Once this PR has been reviewed and has the lgtm label, please assign dsimansk for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@codecov

codecovBot commented Aug 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.14286% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.54%. Comparing base (0fc378e) to head (a5ca744).
⚠️ Report is 2 commits behind head on main.

Files with missing linesPatch %Lines
pkg/apis/operator/base/common.go0.00%2 Missing ⚠️
pkg/reconciler/common/multicluster.go94.73%1 Missing ⚠️
pkg/reconciler/knativeeventing/controller.go0.00%1 Missing ⚠️
pkg/reconciler/knativeserving/controller.go0.00%1 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #2344 +/- ##
==========================================
+ Coverage 63.99% 64.54% +0.55% 
==========================================
Files 55 55 Lines 2491 2502 +11 ==========================================
+ Hits 1594 1615 +21 + Misses 777 761 -16 - Partials 120 126 +6 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@kahirokunn

Copy link
Copy Markdown
MemberAuthor

/retest

@kahirokunn
kahirokunn marked this pull request as draft August 23, 2026 00:47
@knative-prowknative-prowBot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 23, 2026
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 418c364 to 449028eCompareAugust 23, 2026 00:54
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 449028e to 7345ad9CompareAugust 23, 2026 01:11
Signed-off-by: kahirokunn <okinakahiro@gmail.com>
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 7345ad9 to 8447d27CompareAugust 23, 2026 13:24
@kahirokunn
kahirokunn marked this pull request as ready for review August 23, 2026 13:28
@knative-prowknative-prowBot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 23, 2026
@knative-prow
knative-prowBot requested a review from matzewAugust 23, 2026 13:28
Signed-off-by: kahirokunn <okinakahiro@gmail.com>
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 8447d27 to a5ca744CompareAugust 23, 2026 13:29
@dsimansk

Copy link
Copy Markdown
Contributor

@kahirokunn is running multiple CRs of the same kind in parallel namespaces supported use case now? I.e. KnativeServing being on namespace: ns1, namespace: ns2 based on the same cluster profile?

@kahirokunn

Copy link
Copy Markdown
MemberAuthor

No, multiple CRs of the same kind targeting the same cluster are not intended to be supported.
I recalled the earlier discussion that each cluster supports only one Serving and one Eventing installation.
#1472 (comment)

Therefore, allowing an arbitrary destination namespace may be the wrong API design. The management CR may live in any hub namespace, while the remote installation namespace should probably be derived from the kind (knative-serving or knative-eventing). We should also prevent multiple CRs of the same kind from targeting the same ClusterProfile.

@kahirokunn
kahirokunn marked this pull request as draft August 26, 2026 02:16
@knative-prowknative-prowBot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 26, 2026
@kahirokunn

Copy link
Copy Markdown
MemberAuthor

Move to #2349

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge/work-in-progressIndicates that a PR should not merge because it is a work in progress.size/XLDenotes a PR that changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Install remote Knative components in canonical namespaces

2 participants

@kahirokunn@dsimansk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Support explicit remote namespaces via spec.destination - #2344

Closed
kahirokunn wants to merge 2 commits into
knative:mainfrom
kahirokunn:multicluster-destination
Closed

Support explicit remote namespaces via spec.destination#2344
kahirokunn wants to merge 2 commits into
knative:mainfrom
kahirokunn:multicluster-destination

Conversation

@kahirokunn

@kahirokunnkahirokunn commented Aug 22, 2026

Copy link
Copy Markdown
Member

Fixes#2345

Proposed Changes

  • Replace the top-level spec.clusterProfileRef field with an explicit spec.destination object containing both clusterProfileRef and the remote installation namespace.

Design

This PR makes the destination explicit:

spec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

The complete spec.destination is immutable. Moving an existing installation between clusters or namespaces requires deleting and recreating the management CR so that remote finalization runs before the new destination is installed.

Example CR

KnativeServing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-serving-apacnamespace: management-servingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-servingingress:
gateway-api:
enabled: trueistio:
enabled: false

KnativeEventing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeEventingmetadata:
name: knative-eventing-apacnamespace: management-eventingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-eventing

Local cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-servingnamespace: knative-servingspec: {}# No destination: deploys to the local cluster as before.

Verification Steps

E2E verification with Kind

Verified with three Kind clusters (hub, cluster1, and cluster2), OCM-managed ClusterProfile objects and cluster-proxy endpoints, the managed-serviceaccount ClusterProfileCredSyncer, cp-creds, Envoy Gateway, and cloud-provider-kind.

1. Set up OCM and Cluster Inventory API

The environment follows the OCM ClusterProfile guide: three Kind clusters, both spokes registered with OCM, the sandbox-fleet ManagedClusterSet, cluster-proxy, managed-serviceaccount, and ClusterProfile support.

Enable ClusterProfile credential synchronization in the managed-serviceaccount add-on:

helm upgrade managed-serviceaccount ocm/managed-serviceaccount \
--kube-context kind-hub \
-n open-cluster-management-managed-serviceaccount \
--version 0.10.0 \
--reuse-values \
--set featureGates.clusterProfileCredSyncer=true

Bind the operator namespace to the ManagedClusterSet and label both ManagedServiceAccounts for credential synchronization:

apiVersion: cluster.open-cluster-management.io/v1beta2kind: ManagedClusterSetBindingmetadata:
name: sandbox-fleetnamespace: knative-operatorspec:
clusterSet: sandbox-fleet
kubectl --context kind-hub -n cluster1 label managedserviceaccount knative-operator \
authentication.open-cluster-management.io/sync-to-clusterprofile=true
kubectl --context kind-hub -n cluster2 label managedserviceaccount knative-operator \
authentication.open-cluster-management.io/sync-to-clusterprofile=true

Both ClusterProfiles reported ControlPlaneHealthy=True and Joined=True. The managed-serviceaccount add-on synchronized their credentials into the knative-operator namespace.

2. Configure the operator to use cp-creds

The operator mounted quay.io/open-cluster-management/cp-creds:latest as an image volume at /access-plugins/cp-creds and used this provider configuration:

{
"providers": [
{
"name": "open-cluster-management",
"execConfig": {
"apiVersion": "client.authentication.k8s.io/v1",
"command": "/access-plugins/cp-creds/cp-creds",
"args": ["--managed-serviceaccount=knative-operator"],
"provideClusterInfo": true,
"interactiveMode": "Never"
}
}
]
}

3. Build and deploy this PR head

PATH="$(go env GOPATH)/bin:$PATH" \
KO_DOCKER_REPO=kind.local \
KIND_CLUSTER_NAME=hub \
ko apply -f config/
kubectl --context kind-hub -n knative-operator \
rollout status deployment/knative-operator --timeout=180s

Both destination ClusterProfiles resolved through the configured cp-creds provider.

4. Configure the Serving data plane on cluster1

Gateway API v1.4.1 experimental CRDs and Envoy Gateway v1.7.1 were installed on cluster1 using the same GatewayNamespace deployment model as the original multicluster PR. Both GatewayClasses reported Accepted=True, and both Gateways reported Programmed=True.

5. Deploy Serving and Eventing to explicit remote namespaces

The management CR namespaces intentionally differ from the remote installation namespaces:

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: ocm-servingnamespace: management-serving-hubspec:
destination:
clusterProfileRef:
name: cluster1namespace: knative-operatornamespace: knative-servingnamespace:
labels:
operator.knative.dev/ocm-e2e: cluster1annotations:
operator.knative.dev/ocm-e2e-source: destinationingress:
gateway-api:
enabled: trueistio:
enabled: false
---
apiVersion: operator.knative.dev/v1beta1kind: KnativeEventingmetadata:
name: ocm-eventingnamespace: management-eventing-hubspec:
destination:
clusterProfileRef:
name: cluster2namespace: knative-operatornamespace: knative-eventingnamespace:
labels:
operator.knative.dev/ocm-e2e: cluster2annotations:
operator.knative.dev/ocm-e2e-source: destination

Both hub CRs reached TargetClusterResolved=True and Ready=True. All seven Serving Deployments became Available only in cluster1/knative-serving; Knative Eventing reached Ready=True with its controller, webhook, and broker Deployments only in cluster2/knative-eventing; no Knative Deployments appeared in either hub management namespace or on the wrong spoke. The remote namespace metadata was applied, and every managed Deployment was owned by its remote anchor ConfigMap.

6. Verify Serving traffic

kubectl --context kind-cluster1 apply -f - <<'EOF'apiVersion: serving.knative.dev/v1kind: Servicemetadata: name: helloworld-go namespace: defaultspec: template: spec: containers: - image: gcr.io/knative-samples/helloworld-go env: - name: TARGET value: OCM Native cp-creds E2EEOF
kubectl --context kind-cluster1 -n default \
wait --for=condition=Ready kservice/helloworld-go --timeout=300s
LB_IP=$(kubectl --context kind-cluster1 -n eg-external \ get service knative-external \ -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -H 'Host: helloworld-go.default.example.com'"http://${LB_IP}"

Observed response:

Hello OCM Native cp-creds E2E!

The external and cluster-local HTTPRoutes both reported Accepted=True and ResolvedRefs=True.

7. Verify cleanup with a cold cache

To exercise finalization before the ClusterProfile cache was warm, the operator was restarted and both management CRs were deleted immediately. Both remote anchors and their owned Deployments were removed, and both CRs completed deletion.

After recreating the CRs in the same destination namespaces, both returned to TargetClusterResolved=True and Ready=True.

Breaking change

Existing manifests using the unreleased top-level field must be rewritten:

# Beforespec:
clusterProfileRef:
name: cluster1namespace: cluster-inventory# Afterspec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

Release Note

action required: Replace spec.clusterProfileRef with spec.destination.clusterProfileRef and set spec.destination.namespace for remote Knative installations.

@knative-prowknative-prowBot added the size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. label Aug 22, 2026

@knative-prowknative-prowBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kahirokunn: 0 warnings.

Details

In response to this:

Proposed Changes

  • Replace the top-level spec.clusterProfileRef field with an explicit
    spec.destination object containing both clusterProfileRef and the remote
    installation namespace.
  • Reconcile remote manifests, ingress resources, and the anchor ConfigMap into
    spec.destination.namespace instead of coupling the remote installation to
    the management CR namespace.
  • Make the complete destination immutable and validate the ClusterProfile name,
    ClusterProfile namespace, and installation namespace as Kubernetes names.
  • Update generated CRDs, Helm CRDs, documentation, unit tests, and multicluster
    E2E coverage for the new API shape.

This is an intentional breaking replacement of an unreleased API with no
current consumers. No compatibility or migration path for the removed
top-level spec.clusterProfileRef field is retained.

Design

The existing multicluster reconciliation design remains unchanged: the
operator resolves the referenced Cluster Inventory API ClusterProfile, swaps
the manifest client at the start of reconciliation, and uses an anchor
ConfigMap for namespace-scoped garbage collection on the remote cluster.

This PR makes the destination explicit:

spec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

The management CR namespace and remote installation namespace are independent.
spec.namespace.labels and spec.namespace.annotations are merged into the
selected remote namespace while preserving unrelated existing metadata.

The complete spec.destination is immutable. Moving an existing installation
between clusters or namespaces requires deleting and recreating the management
CR so that remote finalization runs before the new destination is installed.

Example CR

KnativeServing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-serving-apacnamespace: management-servingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-servingingress:
gateway-api:
enabled: trueistio:
enabled: false

KnativeEventing on a remote cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeEventingmetadata:
name: knative-eventing-apacnamespace: management-eventingspec:
destination:
clusterProfileRef:
name: apac-cluster-01namespace: fleet-systemnamespace: knative-eventing

Local cluster

apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata:
name: knative-servingnamespace: knative-servingspec: {}# No destination: deploys to the local cluster as before.

Verification Steps

Automated verification

go test ./...
go test -run '^$' -tags='e2e multicluster' ./test/e2e
./hack/verify-codegen.sh

The repository multicluster Kind suite also passed all 11 tests:

SPOKE_CLUSTER_NAME=codex-destination-spoke \
./test/e2e-tests-multicluster.sh --kubeconfig=<hub-kubeconfig>

E2E verification with Kind

The following end-to-end flow was executed against this PR head. It uses three
Kind clusters (hub, cluster1, and cluster2), real OCM-managed
ClusterProfile objects, the official Cluster Inventory API secretreader
plugin, Envoy Gateway, and cloud-provider-kind.

1. Cluster and OCM setup

The OCM and Cluster Inventory API environment was created following the
OCM ClusterProfile guide:

kind create cluster --name hub
kind create cluster --name cluster1
kind create cluster --name cluster2
# Install OCM on hub, join cluster1 and cluster2, and enable ClusterProfile.# Install sandbox-fleet, cluster-proxy, and managed-serviceaccount.
kubectl --context kind-hub get managedclusters
kubectl --context kind-hub -n cluster-inventory get clusterprofiles
kubectl --context kind-hub get managedclusteraddons -A

Verified state:

  • cluster1 and cluster2: Joined=True, Available=True
  • generated ClusterProfiles: ControlPlaneHealthy=True, Joined=True
  • cluster-proxy and managed-serviceaccount: Available=True

2. Spoke credentials and ClusterProfile access

An OCM ManagedServiceAccount named knative-operator was created for each
managed cluster. The generated ClusterProfiles expose the cluster-proxy access
provider:

kubectl --context kind-hub -n cluster1 get managedserviceaccount knative-operator
kubectl --context kind-hub -n cluster-inventory get clusterprofile cluster1 -o yaml

The operator uses the official secretreader plugin:

{
"providers": [
{
"name": "open-cluster-management",
"execConfig": {
"apiVersion": "client.authentication.k8s.io/v1",
"command": "/access-plugins/secretreader/bin/secretreader-plugin",
"provideClusterInfo": true,
"interactiveMode": "Never"
}
}
]
}

The plugin image was
registry.k8s.io/cluster-inventory-api/secretreader:v0.1.3.

3. Operator deployment

PATH="$(go env GOPATH)/bin:$PATH" \
KO_DOCKER_REPO=kind.local \
KIND_CLUSTER_NAME=hub \
ko apply -f config/
kubectl --context kind-hub -n knative-operator \
rollout status deployment/knative-operator --timeout=180s

The operator was configured with:

--clusterprofile-provider-file=/etc/cluster-inventory/config.json
--remote-deployments-poll-interval=2s

The running operator reported commit 418c364 and successfully resolved the
OCM cluster-proxy endpoint for cluster-inventory/cluster1.

4. Install Envoy Gateway on cluster1

Gateway API v1.4.1 experimental CRDs were installed on the spoke. Envoy Gateway
v1.7.1 was then installed using the same GatewayNamespace deployment model as
the original multicluster PR:

cat <<'EOF' > /tmp/values-eg.yamlconfig: envoyGateway: provider: type: Kubernetes kubernetes: deploy: type: GatewayNamespaceEOF
kubectl --context kind-cluster1 create namespace envoy-gateway-system
helm template eg oci://docker.io/envoyproxy/gateway-helm \
--version v1.7.1 \
-n envoy-gateway-system \
-f /tmp/values-eg.yaml \
--include-crds | \
sed -n '/^---$/,$p'| \
kubectl --context kind-cluster1 apply --server-side --force-conflicts -f -
sudo cloud-provider-kind

5. Create external and internal Gateway resources

Two Envoy Gateway instances were created:

  • eg-external/eg-external: LoadBalancer, ports 80 and 443
  • eg-internal/eg-internal: ClusterIP, port 80
apiVersion: v1kind: Namespacemetadata:
name: eg-external
---
apiVersion: gateway.envoyproxy.io/v1alpha1kind: EnvoyProxymetadata:
name: knative-external-confignamespace: envoy-gateway-systemspec:
provider:
type: Kuberneteskubernetes:
envoyService:
name: knative-external
---
apiVersion: gateway.networking.k8s.io/v1kind: GatewayClassmetadata:
name: eg-externalspec:
controllerName: gateway.envoyproxy.io/gatewayclass-controllerparametersRef:
group: gateway.envoyproxy.iokind: EnvoyProxyname: knative-external-confignamespace: envoy-gateway-system
---
apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata:
name: eg-externalnamespace: eg-externalspec:
gatewayClassName: eg-externallisteners:
- name: httpport: 80protocol: HTTPallowedRoutes:
namespaces:
from: All
- name: tlsport: 443protocol: TLStls:
mode: PassthroughallowedRoutes:
namespaces:
from: All
---
apiVersion: v1kind: Namespacemetadata:
name: eg-internal
---
apiVersion: gateway.envoyproxy.io/v1alpha1kind: EnvoyProxymetadata:
name: knative-internal-confignamespace: envoy-gateway-systemspec:
provider:
type: Kuberneteskubernetes:
envoyService:
type: ClusterIPname: knative-internal
---
apiVersion: gateway.networking.k8s.io/v1kind: GatewayClassmetadata:
name: eg-internalspec:
controllerName: gateway.envoyproxy.io/gatewayclass-controllerparametersRef:
group: gateway.envoyproxy.iokind: EnvoyProxyname: knative-internal-confignamespace: envoy-gateway-system
---
apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata:
name: eg-internalnamespace: eg-internalspec:
gatewayClassName: eg-internallisteners:
- name: httpport: 80protocol: HTTPallowedRoutes:
namespaces:
from: All

Both GatewayClasses reached Accepted=True; both Gateways reached
Programmed=True. The external service received 172.18.0.12 from
cloud-provider-kind.

6. Deploy KnativeServing with an explicit destination

kubectl --context kind-hub apply -f - <<'EOF'apiVersion: operator.knative.dev/v1beta1kind: KnativeServingmetadata: name: ocm-serving namespace: management-serving-hubspec: destination: clusterProfileRef: name: cluster1 namespace: cluster-inventory namespace: knative-serving-ocm ingress: gateway-api: enabled: true istio: enabled: false config: config-gateway: external-gateways: | - class: eg-external gateway: eg-external/eg-external service: eg-external/knative-external supported-features: - HTTPRouteRequestTimeout local-gateways: | - class: eg-internal gateway: eg-internal/eg-internal service: eg-internal/knative-internal supported-features: - HTTPRouteRequestTimeout network: ingress-class: gateway-api.ingress.networking.knative.dev domain: example.com: ""EOF

The hub CR reached Ready=True and TargetClusterResolved=True. All seven
Knative Serving Deployments became Available in
cluster1/knative-serving-ocm, with no Serving Deployment in the hub management
namespace or on cluster2.

7. Verify ownership and data-plane traffic

The remote anchor was present and protected:

kubectl --context kind-cluster1 -n knative-serving-ocm \
get configmap knativeserving-ocm-serving-root-owner -o yaml
kubectl --context kind-cluster1 -n knative-serving-ocm \
get deployment activator -o jsonpath='{.metadata.ownerReferences}'| jq .

The activator Deployment ownerReference matched the anchor ConfigMap UID.

A Knative Service was then deployed on cluster1:

kubectl --context kind-cluster1 apply -f - <<'EOF'apiVersion: serving.knative.dev/v1kind: Servicemetadata: name: helloworld-go namespace: defaultspec: template: spec: containers: - image: gcr.io/knative-samples/helloworld-go env: - name: TARGET value: Destination Kind E2EEOF
kubectl --context kind-cluster1 -n default \
wait --for=condition=Ready kservice/helloworld-go --timeout=300s
LB_IP=$(kubectl --context kind-cluster1 -n eg-external \ get service knative-external \ -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -H 'Host: helloworld-go.default.example.com'"http://${LB_IP}"

Observed response:

Hello Destination Kind E2E!

The external and cluster-local HTTPRoutes both reported Accepted=True.

8. Verify CR deletion and finalizer cleanup

kubectl --context kind-cluster1 -n default delete kservice helloworld-go
kubectl --context kind-hub -n management-serving-hub \
delete knativeserving ocm-serving

Verified after finalization:

  • the hub KnativeServing CR was deleted
  • the remote anchor ConfigMap was deleted
  • all labeled Knative Serving Deployments were deleted
  • all Knative Serving ClusterRoles were deleted

The dynamically created autoscaler-bucket-00-of-01 Service and leader-election
Leases do not carry the anchor ownerReference and can remain after uninstall.
This also occurs on fork/main and is not introduced by the destination API
replacement. Those test-only remnants were removed before reinstalling into the
same namespace.

9. Recreate and leave the environment healthy

The same KnativeServing CR was recreated after cleanup. It returned to
Ready=True and TargetClusterResolved=True, and all seven Serving Deployments
became Available again. The OCM hub, both managed clusters, Envoy Gateway,
Knative Serving, and Knative Eventing were left running for inspection.

Destination validation

The real API server rejected all of the following:

  • invalid destination namespace
  • missing ClusterProfile name, ClusterProfile namespace, or target namespace
  • legacy top-level spec.clusterProfileRef
  • changes to destination.namespace
  • changes to destination.clusterProfileRef.name
  • changes to destination.clusterProfileRef.namespace
  • removal of spec.destination

Intentional breaking change

Existing manifests using the unreleased top-level field must be rewritten:

# Beforespec:
clusterProfileRef:
name: cluster1namespace: cluster-inventory# Afterspec:
destination:
clusterProfileRef:
name: cluster1namespace: cluster-inventorynamespace: knative-serving

No automatic migration or compatibility field is provided because the old API
has no current consumers.

Release Note

action required: Replace spec.clusterProfileRef with spec.destination.clusterProfileRef and set spec.destination.namespace for remote Knative installations.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@knative-prow

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: kahirokunn
Once this PR has been reviewed and has the lgtm label, please assign dsimansk for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@codecov

codecovBot commented Aug 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.14286% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.54%. Comparing base (0fc378e) to head (a5ca744).
⚠️ Report is 2 commits behind head on main.

Files with missing linesPatch %Lines
pkg/apis/operator/base/common.go0.00%2 Missing ⚠️
pkg/reconciler/common/multicluster.go94.73%1 Missing ⚠️
pkg/reconciler/knativeeventing/controller.go0.00%1 Missing ⚠️
pkg/reconciler/knativeserving/controller.go0.00%1 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #2344 +/- ##
==========================================
+ Coverage 63.99% 64.54% +0.55% 
==========================================
Files 55 55 Lines 2491 2502 +11 ==========================================
+ Hits 1594 1615 +21 + Misses 777 761 -16 - Partials 120 126 +6 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@kahirokunn

Copy link
Copy Markdown
MemberAuthor

/retest

@kahirokunn
kahirokunn marked this pull request as draft August 23, 2026 00:47
@knative-prowknative-prowBot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 23, 2026
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 418c364 to 449028eCompareAugust 23, 2026 00:54
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 449028e to 7345ad9CompareAugust 23, 2026 01:11
Signed-off-by: kahirokunn <okinakahiro@gmail.com>
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 7345ad9 to 8447d27CompareAugust 23, 2026 13:24
@kahirokunn
kahirokunn marked this pull request as ready for review August 23, 2026 13:28
@knative-prowknative-prowBot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 23, 2026
@knative-prow
knative-prowBot requested a review from matzewAugust 23, 2026 13:28
Signed-off-by: kahirokunn <okinakahiro@gmail.com>
@kahirokunn
kahirokunnforce-pushed the multicluster-destination branch from 8447d27 to a5ca744CompareAugust 23, 2026 13:29
@dsimansk

Copy link
Copy Markdown
Contributor

@kahirokunn is running multiple CRs of the same kind in parallel namespaces supported use case now? I.e. KnativeServing being on namespace: ns1, namespace: ns2 based on the same cluster profile?

@kahirokunn

Copy link
Copy Markdown
MemberAuthor

No, multiple CRs of the same kind targeting the same cluster are not intended to be supported.
I recalled the earlier discussion that each cluster supports only one Serving and one Eventing installation.
#1472 (comment)

Therefore, allowing an arbitrary destination namespace may be the wrong API design. The management CR may live in any hub namespace, while the remote installation namespace should probably be derived from the kind (knative-serving or knative-eventing). We should also prevent multiple CRs of the same kind from targeting the same ClusterProfile.

@kahirokunn
kahirokunn marked this pull request as draft August 26, 2026 02:16
@knative-prowknative-prowBot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 26, 2026
@kahirokunn

Copy link
Copy Markdown
MemberAuthor

Move to #2349

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge/work-in-progressIndicates that a PR should not merge because it is a work in progress.size/XLDenotes a PR that changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Install remote Knative components in canonical namespaces

2 participants

@kahirokunn@dsimansk