From 0d687bc72d8034d815c9af2d7d1b935bf09c3b06 Mon Sep 17 00:00:00 2001 From: kahirokunn Date: Wed, 26 Aug 2026 14:11:23 +0900 Subject: [PATCH 1/2] Install remote Knative components in canonical namespaces Signed-off-by: kahirokunn --- docs/development/e2e-multicluster.md | 19 ++-- docs/multicluster.md | 49 ++++++--- pkg/reconciler/common/multicluster.go | 28 ++++- pkg/reconciler/common/multicluster_test.go | 101 ++++++++++++++++++ pkg/reconciler/common/transformers.go | 4 +- pkg/reconciler/common/transformers_test.go | 59 ++++++++++ .../knativeserving/ingress/ingress_service.go | 6 +- .../ingress/ingress_service_test.go | 25 +++++ test/cleanup.go | 4 +- test/e2e-common.sh | 10 +- test/e2e/knativeeventing_spoke_test.go | 28 ++--- test/e2e/knativeserving_spoke_test.go | 24 +++-- test/resources/verify.go | 4 +- 13 files changed, 294 insertions(+), 67 deletions(-) diff --git a/docs/development/e2e-multicluster.md b/docs/development/e2e-multicluster.md index d14fafce54..b62faa13db 100644 --- a/docs/development/e2e-multicluster.md +++ b/docs/development/e2e-multicluster.md @@ -107,13 +107,13 @@ Create the manifest in a temp file (the repository does not ship a `hack/manual/` directory) and apply it: ```bash -kubectl create ns knative-serving +kubectl create ns fleet-workloads cat > /tmp/knativeserving-spoke.yaml <<'EOF' apiVersion: operator.knative.dev/v1beta1 kind: KnativeServing metadata: - name: knative-serving - namespace: knative-serving + name: serving-spoke + namespace: fleet-workloads spec: clusterProfileRef: name: spoke @@ -122,16 +122,15 @@ EOF kubectl apply -f /tmp/knativeserving-spoke.yaml ``` -Same pattern for Eventing: +Apply the Eventing management CR in the same hub namespace: ```bash -kubectl create ns knative-eventing cat > /tmp/knativeeventing-spoke.yaml <<'EOF' apiVersion: operator.knative.dev/v1beta1 kind: KnativeEventing metadata: - name: knative-eventing - namespace: knative-eventing + name: eventing-spoke + namespace: fleet-workloads spec: clusterProfileRef: name: spoke @@ -146,7 +145,7 @@ Watch both status conditions and the spoke anchor: kubectl get knativeserving -A -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.conditions[?(@.type=="TargetClusterResolved")].status}{"\t"}{.status.conditions[?(@.type=="InstallSucceeded")].status}{"\n"}{end}' KUBECONFIG="${SPOKE_HOST_KUBECONFIG}" kubectl get cm -A \ - -l operator.knative.dev/cr-name=knative-serving + -l operator.knative.dev/cr-name=serving-spoke KUBECONFIG="${SPOKE_HOST_KUBECONFIG}" kubectl -n knative-serving rollout status deploy/activator ``` @@ -156,8 +155,8 @@ KUBECONFIG="${SPOKE_HOST_KUBECONFIG}" kubectl -n knative-serving rollout status Delete the hub CRs in reverse order; the operator's finalizer cleans the spoke: ```bash -kubectl delete knativeeventing -n knative-eventing knative-eventing -kubectl delete knativeserving -n knative-serving knative-serving +kubectl delete knativeeventing -n fleet-workloads eventing-spoke +kubectl delete knativeserving -n fleet-workloads serving-spoke # Anchors should be gone. KUBECONFIG="${SPOKE_HOST_KUBECONFIG}" kubectl get cm -A \ diff --git a/docs/multicluster.md b/docs/multicluster.md index a2e18fcef6..67a9bad9cd 100644 --- a/docs/multicluster.md +++ b/docs/multicluster.md @@ -1,9 +1,8 @@ # Multi-Cluster Deployment The operator can deploy Knative Serving and Eventing to remote clusters from a -single hub cluster. A `KnativeServing` or `KnativeEventing` CR carrying a -`spec.clusterProfileRef` reconciles on the referenced spoke cluster; without it -the operator behaves as before. +single hub cluster. Platform administrators keep the management CRs on the hub +and use `spec.clusterProfileRef` to choose the spoke cluster. The hub needs network access to each spoke API server. Connection details are resolved through the Cluster Inventory API (`ClusterProfile`). @@ -19,20 +18,35 @@ resolved through the Cluster Inventory API (`ClusterProfile`). ## Usage -Set `spec.clusterProfileRef` on a CR to target a remote cluster: +Create the management CR in a hub namespace used by your platform team and set +`spec.clusterProfileRef` to the spoke cluster. For example: ```yaml apiVersion: operator.knative.dev/v1beta1 kind: KnativeServing metadata: - name: knative-serving - namespace: knative-serving + name: serving-spoke-tokyo + namespace: fleet-workloads spec: clusterProfileRef: - name: spoke-cluster-1 + name: spoke-tokyo namespace: fleet-system ``` +Remote placement is fixed by component kind. The management CR namespace does +not select the spoke installation namespace: + +| Management CR on the hub | `ClusterProfile` on the hub | Installation namespace on the spoke | +|--------------------------|-----------------------------|-------------------------------------| +| `fleet-workloads/serving-spoke-tokyo` (`KnativeServing`) | `fleet-system/spoke-tokyo` | `knative-serving` | +| `fleet-workloads/eventing-spoke-tokyo` (`KnativeEventing`) | `fleet-system/spoke-tokyo` | `knative-eventing` | + +The operator does not create a `KnativeServing` or `KnativeEventing` CR on the +spoke. It renders and applies the release manifests from the hub CR directly. + +When `spec.clusterProfileRef` is omitted, the installation remains local and +continues to use the management CR's `metadata.namespace`. + The operator resolves the `ClusterProfile`, builds a `rest.Config` via the configured access provider, and applies manifests on the spoke. A `TargetClusterResolved` status condition tracks whether the remote cluster was @@ -80,17 +94,18 @@ point under a plugin mount path, not at the mount directory itself. ## Namespace configuration -`spec.namespaceConfiguration.labels` and `spec.namespaceConfiguration.annotations` -are applied to the spoke namespace when the operator creates it. Existing -spoke namespaces are not modified. +`spec.namespace.labels` and `spec.namespace.annotations` are applied to the +spoke installation namespace shown in the Usage table, not the management CR +namespace. ## Anchor ConfigMap For remote deployments, the operator creates an anchor ConfigMap -(`{kind}-{cr-name}-root-owner`) on the spoke. Namespace-scoped resources use -it as their `OwnerReference`, so deleting the anchor triggers GC of all owned -resources. Cluster-scoped resources are not owned by the anchor and are -cleaned up by `FinalizeRemoteCluster` when the hub CR is deleted. +(`{kind}-{cr-name}-root-owner`) in the spoke installation namespace shown in the +Usage table. Namespace-scoped resources use it as their `OwnerReference`, so +deleting the anchor triggers GC of all owned resources. Cluster-scoped resources +are not owned by the anchor and are cleaned up by the operator when the hub CR +is deleted. The anchor carries an `operator.knative.dev/protected=true` annotation and a description annotation warning against manual deletion. To uninstall safely, @@ -126,7 +141,8 @@ knative_operator: Check the status condition on the CR: ```bash -kubectl get knativeserving -n -o jsonpath='{.status.conditions[?(@.type=="TargetClusterResolved")]}' +kubectl get knativeserving -n fleet-workloads serving-spoke-tokyo \ + -o jsonpath='{.status.conditions[?(@.type=="TargetClusterResolved")]}' ``` Common reasons for `TargetClusterResolved=False`: @@ -151,4 +167,5 @@ Common reasons for `TargetClusterResolved=False`: If spoke deployments are not coming up, confirm `TargetClusterResolved=True`, check the operator logs on the hub, and inspect the spoke cluster directly with -`kubectl --kubeconfig= get deployments -n `. +`kubectl --kubeconfig=spoke-tokyo.kubeconfig get deployments -n knative-serving` +or `-n knative-eventing`. diff --git a/pkg/reconciler/common/multicluster.go b/pkg/reconciler/common/multicluster.go index b135670eb1..a9cd5fac04 100644 --- a/pkg/reconciler/common/multicluster.go +++ b/pkg/reconciler/common/multicluster.go @@ -39,6 +39,7 @@ import ( "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" + "knative.dev/operator/pkg/apis/operator" "knative.dev/operator/pkg/apis/operator/base" "knative.dev/pkg/logging" @@ -48,9 +49,11 @@ import ( ) const ( - defaultRemoteClusterTimeout = 10 * time.Second - remoteClusterQPS = float32(20) - remoteClusterBurst = 40 + defaultRemoteClusterTimeout = 10 * time.Second + remoteClusterQPS = float32(20) + remoteClusterBurst = 40 + servingInstallationNamespace = "knative-serving" + eventingInstallationNamespace = "knative-eventing" ) var ( @@ -428,6 +431,21 @@ func ShouldFinalizeClusterScoped( return true } +// InstallationNamespace returns the namespace where the component's manifests +// are installed. Local installations use the management CR namespace. Remote +// installations use the canonical namespace for the component kind. +func InstallationNamespace(instance base.KComponent) string { + if instance.GetSpec().GetClusterProfileRef() != nil { + switch instance.GroupVersionKind().Kind { + case operator.KindKnativeServing: + return servingInstallationNamespace + case operator.KindKnativeEventing: + return eventingInstallationNamespace + } + } + return instance.GetNamespace() +} + func SameClusterProfile(a, b *base.ClusterProfileReference) bool { if a == nil && b == nil { return true @@ -534,7 +552,7 @@ func EnsureAnchorConfigMap( if len(name) > maxResourceNameLength { return nil, fmt.Errorf("anchor ConfigMap name %q exceeds maximum length of %d characters; shorten the CR name", name, maxResourceNameLength) } - ns := instance.GetNamespace() + ns := InstallationNamespace(instance) nsCfg := instance.GetSpec().GetNamespaceConfiguration() if _, err := kubeClient.CoreV1().Namespaces().Get(ctx, ns, metav1.GetOptions{}); err != nil { @@ -640,7 +658,7 @@ func DeleteAnchorConfigMap( instance base.KComponent, ) error { name := AnchorName(instance) - ns := instance.GetNamespace() + ns := InstallationNamespace(instance) err := kubeClient.CoreV1().ConfigMaps(ns).Delete(ctx, name, metav1.DeleteOptions{}) if err != nil && !apierrors.IsNotFound(err) { return fmt.Errorf("failed to delete anchor ConfigMap %s/%s: %w", ns, name, err) diff --git a/pkg/reconciler/common/multicluster_test.go b/pkg/reconciler/common/multicluster_test.go index ee12cee4a4..5f2cdc8eff 100644 --- a/pkg/reconciler/common/multicluster_test.go +++ b/pkg/reconciler/common/multicluster_test.go @@ -27,6 +27,7 @@ import ( mf "github.com/manifestival/manifestival" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -41,6 +42,58 @@ import ( clusterinventoryv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" ) +func TestInstallationNamespace(t *testing.T) { + remoteRef := &base.ClusterProfileReference{Name: "spoke", Namespace: "fleet-system"} + tests := []struct { + name string + instance base.KComponent + want string + }{ + { + name: "local serving uses management namespace", + instance: &v1beta1.KnativeServing{ + ObjectMeta: metav1.ObjectMeta{Namespace: "serving-management"}, + }, + want: "serving-management", + }, + { + name: "remote serving uses canonical namespace", + instance: &v1beta1.KnativeServing{ + ObjectMeta: metav1.ObjectMeta{Namespace: "serving-management"}, + Spec: v1beta1.KnativeServingSpec{ + CommonSpec: base.CommonSpec{ClusterProfileRef: remoteRef}, + }, + }, + want: "knative-serving", + }, + { + name: "local eventing uses management namespace", + instance: &v1beta1.KnativeEventing{ + ObjectMeta: metav1.ObjectMeta{Namespace: "eventing-management"}, + }, + want: "eventing-management", + }, + { + name: "remote eventing uses canonical namespace", + instance: &v1beta1.KnativeEventing{ + ObjectMeta: metav1.ObjectMeta{Namespace: "eventing-management"}, + Spec: v1beta1.KnativeEventingSpec{ + CommonSpec: base.CommonSpec{ClusterProfileRef: remoteRef}, + }, + }, + want: "knative-eventing", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := InstallationNamespace(tt.instance); got != tt.want { + t.Fatalf("InstallationNamespace() = %q, want %q", got, tt.want) + } + }) + } +} + func TestResolveTargetCluster_NilRef(t *testing.T) { instance := &v1beta1.KnativeServing{ ObjectMeta: metav1.ObjectMeta{ @@ -260,6 +313,54 @@ func TestDeleteAnchorConfigMap_Success(t *testing.T) { } } +func TestEnsureAndDeleteAnchorConfigMap_RemoteCanonicalNamespace(t *testing.T) { + instance := &v1beta1.KnativeServing{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "serving-management", + Name: "test", + }, + Spec: v1beta1.KnativeServingSpec{ + CommonSpec: base.CommonSpec{ + ClusterProfileRef: &base.ClusterProfileReference{ + Name: "spoke", + Namespace: "fleet-system", + }, + NamespaceConfiguration: &base.NamespaceConfiguration{ + Labels: map[string]string{"team": "platform"}, + }, + }, + }, + } + kubeClient := fake.NewSimpleClientset() + ctx := context.Background() + + anchor, err := EnsureAnchorConfigMap(ctx, kubeClient, instance) + if err != nil { + t.Fatalf("EnsureAnchorConfigMap() error: %v", err) + } + if got, want := anchor.Namespace, "knative-serving"; got != want { + t.Fatalf("anchor.Namespace = %q, want %q", got, want) + } + ns, err := kubeClient.CoreV1().Namespaces().Get(ctx, "knative-serving", metav1.GetOptions{}) + if err != nil { + t.Fatalf("Get canonical namespace: %v", err) + } + if got := ns.Labels["team"]; got != "platform" { + t.Fatalf("canonical namespace team label = %q, want %q", got, "platform") + } + if _, err := kubeClient.CoreV1().Namespaces().Get(ctx, "serving-management", metav1.GetOptions{}); !apierrors.IsNotFound(err) { + t.Fatalf("management namespace lookup error = %v, want NotFound", err) + } + + if err := DeleteAnchorConfigMap(ctx, kubeClient, instance); err != nil { + t.Fatalf("DeleteAnchorConfigMap() error: %v", err) + } + if _, err := kubeClient.CoreV1().ConfigMaps("knative-serving").Get( + ctx, AnchorName(instance), metav1.GetOptions{}); !apierrors.IsNotFound(err) { + t.Fatalf("canonical namespace anchor lookup error = %v, want NotFound", err) + } +} + func TestConfigEqual(t *testing.T) { cfg := &rest.Config{ Host: "https://example.com", diff --git a/pkg/reconciler/common/transformers.go b/pkg/reconciler/common/transformers.go index 5bf1965d14..33eadc7389 100644 --- a/pkg/reconciler/common/transformers.go +++ b/pkg/reconciler/common/transformers.go @@ -28,7 +28,7 @@ import ( func transformers(ctx context.Context, obj base.KComponent) []mf.Transformer { logger := logging.FromContext(ctx) return []mf.Transformer{ - mf.InjectNamespace(obj.GetNamespace()), + mf.InjectNamespace(InstallationNamespace(obj)), NamespaceConfigurationTransform(obj.GetSpec().GetNamespaceConfiguration()), HighAvailabilityTransform(obj), ImageTransform(obj.GetSpec().GetRegistry(), logger), @@ -80,7 +80,7 @@ func Transform(ctx context.Context, manifest *mf.Manifest, instance base.KCompon // InjectNamespace will mutate the namespace of all installed resources func InjectNamespace(manifest *mf.Manifest, instance base.KComponent, extra ...mf.Transformer) error { transformers := make([]mf.Transformer, 0, 1+len(extra)) - transformers = append(transformers, mf.InjectNamespace(instance.GetNamespace())) + transformers = append(transformers, mf.InjectNamespace(InstallationNamespace(instance))) transformers = append(transformers, extra...) m, err := manifest.Transform(transformers...) if err != nil { diff --git a/pkg/reconciler/common/transformers_test.go b/pkg/reconciler/common/transformers_test.go index 499163fa8d..d8796d7080 100644 --- a/pkg/reconciler/common/transformers_test.go +++ b/pkg/reconciler/common/transformers_test.go @@ -26,6 +26,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/types" + "knative.dev/operator/pkg/apis/operator/base" "knative.dev/operator/pkg/apis/operator/v1beta1" "knative.dev/pkg/ptr" ) @@ -84,6 +85,35 @@ func TestCommonTransformers(t *testing.T) { } } +func TestCommonTransformers_RemoteCanonicalNamespace(t *testing.T) { + component := &v1beta1.KnativeServing{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "serving-management", + Name: "test-name", + }, + Spec: v1beta1.KnativeServingSpec{ + CommonSpec: base.CommonSpec{ + ClusterProfileRef: &base.ClusterProfileReference{ + Name: "spoke", + Namespace: "fleet-system", + }, + }, + }, + } + in := []unstructured.Unstructured{*NamespacedResource("test/v1", "TestCR", "another-ns", "test-resource")} + manifest, err := mf.ManifestFrom(mf.Slice(in)) + if err != nil { + t.Fatalf("Failed to generate manifest: %v", err) + } + if err := Transform(context.Background(), &manifest, component); err != nil { + t.Fatalf("Transform() error: %v", err) + } + + if got, want := manifest.Resources()[0].GetNamespace(), "knative-serving"; got != want { + t.Fatalf("resource namespace = %q, want %q", got, want) + } +} + func TestInjectOwner_UsesAnchorWhenSet(t *testing.T) { component := &v1beta1.KnativeEventing{ ObjectMeta: metav1.ObjectMeta{ @@ -246,3 +276,32 @@ func TestInjectNamespace(t *testing.T) { t.Fatalf("GetNamespace() = %s, want %s", got, want) } } + +func TestInjectNamespace_RemoteCanonicalNamespace(t *testing.T) { + component := &v1beta1.KnativeEventing{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eventing-management", + Name: "test-name", + }, + Spec: v1beta1.KnativeEventingSpec{ + CommonSpec: base.CommonSpec{ + ClusterProfileRef: &base.ClusterProfileReference{ + Name: "spoke", + Namespace: "fleet-system", + }, + }, + }, + } + in := []unstructured.Unstructured{*NamespacedResource("test/v1", "TestCR", "another-ns", "test-resource")} + manifest, err := mf.ManifestFrom(mf.Slice(in)) + if err != nil { + t.Fatalf("Failed to generate manifest: %v", err) + } + if err := InjectNamespace(&manifest, component); err != nil { + t.Fatalf("InjectNamespace() error: %v", err) + } + + if got, want := manifest.Resources()[0].GetNamespace(), "knative-eventing"; got != want { + t.Fatalf("resource namespace = %q, want %q", got, want) + } +} diff --git a/pkg/reconciler/knativeserving/ingress/ingress_service.go b/pkg/reconciler/knativeserving/ingress/ingress_service.go index 4442e17d3d..b07cb66b86 100644 --- a/pkg/reconciler/knativeserving/ingress/ingress_service.go +++ b/pkg/reconciler/knativeserving/ingress/ingress_service.go @@ -23,6 +23,7 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes/scheme" "knative.dev/operator/pkg/apis/operator/base" + "knative.dev/operator/pkg/reconciler/common" "sigs.k8s.io/yaml" mf "github.com/manifestival/manifestival" @@ -44,17 +45,18 @@ func IngressServiceTransform(ks *v1beta1.KnativeServing) mf.Transformer { return func(u *unstructured.Unstructured) error { if u.GetAPIVersion() == "v1" && u.GetKind() == "Service" { if u.GetName() == "knative-local-gateway" { + installationNamespace := common.InstallationNamespace(ks) // Default to istio-system, then override if config exists u.SetNamespace("istio-system") u.SetOwnerReferences(nil) config := ks.GetSpec().GetConfig() if data, ok := config["istio"]; ok { - UpdateNamespace(u, data, ks.GetNamespace()) + UpdateNamespace(u, data, installationNamespace) } // The "config-" prefix is optional if data, ok := config["config-istio"]; ok { - UpdateNamespace(u, data, ks.GetNamespace()) + UpdateNamespace(u, data, installationNamespace) } return updateIstioService(ks, u, localGateway) diff --git a/pkg/reconciler/knativeserving/ingress/ingress_service_test.go b/pkg/reconciler/knativeserving/ingress/ingress_service_test.go index 728cb38698..092050e441 100644 --- a/pkg/reconciler/knativeserving/ingress/ingress_service_test.go +++ b/pkg/reconciler/knativeserving/ingress/ingress_service_test.go @@ -149,6 +149,31 @@ func TestIngressServiceTransformIstioSelector(t *testing.T) { }, }, expected: true, + }, { + name: "RemoteServingUsesCanonicalNamespaceForLegacyGatewayConfig", + namespace: "test-namespace", + serviceName: "knative-local-gateway", + expectedNamespace: "istio-system-remote", + instance: &servingv1beta1.KnativeServing{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-instance", + Namespace: "serving-management", + }, + Spec: servingv1beta1.KnativeServingSpec{ + CommonSpec: base.CommonSpec{ + ClusterProfileRef: &base.ClusterProfileReference{ + Name: "spoke", + Namespace: "fleet-system", + }, + Config: map[string]map[string]string{ + "istio": { + "local-gateway.knative-serving.knative-local-gateway": "knative-local-gateway.istio-system-remote.svc.cluster.local", + }, + }, + }, + }, + }, + expected: true, }, { name: "IstioNotUnderDefaultNS with invalid config-istio data", namespace: "test-namespace", diff --git a/test/cleanup.go b/test/cleanup.go index 61832047d4..e06bdc539b 100644 --- a/test/cleanup.go +++ b/test/cleanup.go @@ -42,7 +42,7 @@ func CleanupOnInterrupt(cleanup func()) { // TearDown will delete created names using clients. func TearDown(clients *Clients, names ResourceNames) { if clients != nil && clients.Operator != nil { - clients.KnativeServing().Delete(context.TODO(), names.KnativeServing, metav1.DeleteOptions{}) - clients.KnativeEventing().Delete(context.TODO(), names.KnativeEventing, metav1.DeleteOptions{}) + clients.Operator.KnativeServings(names.Namespace).Delete(context.TODO(), names.KnativeServing, metav1.DeleteOptions{}) + clients.Operator.KnativeEventings(names.Namespace).Delete(context.TODO(), names.KnativeEventing, metav1.DeleteOptions{}) } } diff --git a/test/e2e-common.sh b/test/e2e-common.sh index 972eb0e557..91072c72c0 100755 --- a/test/e2e-common.sh +++ b/test/e2e-common.sh @@ -415,10 +415,12 @@ function dump_spoke_state() { echo "=== kubectl get events -A ===" KUBECONFIG="${SPOKE_HOST_KUBECONFIG}" "${kc[@]}" get events -A --sort-by=.lastTimestamp || true echo - local spoke_ns="${TEST_NAMESPACE:-knative-operator-testing}" - echo "=== pod logs in namespace ${spoke_ns} ===" - KUBECONFIG="${SPOKE_HOST_KUBECONFIG}" "${kc[@]}" -n "${spoke_ns}" \ - logs --all-containers=true --prefix --tail=200 -l "app" 2>/dev/null || true + local spoke_ns + for spoke_ns in knative-serving knative-eventing; do + echo "=== pod logs in namespace ${spoke_ns} ===" + KUBECONFIG="${SPOKE_HOST_KUBECONFIG}" "${kc[@]}" -n "${spoke_ns}" \ + logs --all-containers=true --prefix --tail=200 -l "app" 2>/dev/null || true + done } > "${out}" 2>&1 || true } diff --git a/test/e2e/knativeeventing_spoke_test.go b/test/e2e/knativeeventing_spoke_test.go index 0c545e31e4..482fba94d5 100644 --- a/test/e2e/knativeeventing_spoke_test.go +++ b/test/e2e/knativeeventing_spoke_test.go @@ -46,12 +46,14 @@ func TestMulticlusterKnativeEventingSpokeDeployment(t *testing.T) { hub := client.Setup(t) spoke := client.SetupSpoke(t) + // Use a different hub namespace so the test cannot pass because the + // management namespace happens to match the spoke installation namespace. names := test.ResourceNames{ KnativeEventing: test.OperatorName, - Namespace: test.EventingOperatorNamespace, + Namespace: test.ServingOperatorNamespace, } - ensureSpokeNamespace(ctx, t, spoke, names.Namespace) + ensureSpokeNamespace(ctx, t, spoke, spokeEventingInstallNamespace) test.CleanupOnInterrupt(func() { test.TearDown(hub, names) }) defer test.TearDown(hub, names) @@ -71,7 +73,7 @@ func TestMulticlusterKnativeEventingSpokeDeployment(t *testing.T) { t.Run("tls-resources-filtered-without-cert-manager", func(t *testing.T) { ctx := t.Context() - assertNoCertManagerResourcesOnSpoke(ctx, t, spoke, names.Namespace) + assertNoCertManagerResourcesOnSpoke(ctx, t, spoke, spokeEventingInstallNamespace) }) t.Run("delete-and-cleanup-spoke", func(t *testing.T) { @@ -79,13 +81,13 @@ func TestMulticlusterKnativeEventingSpokeDeployment(t *testing.T) { if err := deleteHubKnativeEventing(ctx, hub, names); err != nil { t.Fatalf("Failed to delete hub KnativeEventing %q: %v", names.KnativeEventing, err) } - if err := waitForSpokeDeploymentsGone(ctx, t, spoke, names.Namespace); err != nil { + if err := waitForSpokeDeploymentsGone(ctx, t, spoke, spokeEventingInstallNamespace); err != nil { t.Fatalf("Spoke deployments still present after deletion in namespace %q: %v", - names.Namespace, err) + spokeEventingInstallNamespace, err) } - waitForSpokeManagedServicesGone(ctx, t, spoke, names.Namespace, + waitForSpokeManagedServicesGone(ctx, t, spoke, spokeEventingInstallNamespace, "app.kubernetes.io/name=knative-eventing") - assertAnchorConfigMapGone(ctx, t, spoke, names.Namespace, + assertAnchorConfigMapGone(ctx, t, spoke, spokeEventingInstallNamespace, "knativeeventing-"+names.KnativeEventing+"-root-owner") }) } @@ -105,7 +107,7 @@ func createKnativeEventingWithSpokeRef(ctx context.Context, clients *test.Client }, }, } - _, err := clients.KnativeEventing().Create(ctx, ke, metav1.CreateOptions{}) + _, err := clients.Operator.KnativeEventings(names.Namespace).Create(ctx, ke, metav1.CreateOptions{}) if apierrs.IsAlreadyExists(err) { return nil } @@ -113,7 +115,7 @@ func createKnativeEventingWithSpokeRef(ctx context.Context, clients *test.Client } func deleteHubKnativeEventing(ctx context.Context, clients *test.Clients, names test.ResourceNames) error { - if err := clients.KnativeEventing().Delete(ctx, names.KnativeEventing, metav1.DeleteOptions{}); err != nil { + if err := clients.Operator.KnativeEventings(names.Namespace).Delete(ctx, names.KnativeEventing, metav1.DeleteOptions{}); err != nil { if apierrs.IsNotFound(err) { return nil } @@ -121,7 +123,7 @@ func deleteHubKnativeEventing(ctx context.Context, clients *test.Clients, names } return wait.PollUntilContextTimeout(ctx, spokeWaitInterval, spokeGoneTimeout, true, func(ctx context.Context) (bool, error) { - _, err := clients.KnativeEventing().Get(ctx, names.KnativeEventing, metav1.GetOptions{}) + _, err := clients.Operator.KnativeEventings(names.Namespace).Get(ctx, names.KnativeEventing, metav1.GetOptions{}) if apierrs.IsNotFound(err) { return true, nil } @@ -138,7 +140,7 @@ func waitForSpokeEventingDeploymentsReady(ctx context.Context, t *testing.T, hub var lastResolveStatus string resolveErr := wait.PollUntilContextTimeout(ctx, spokeWaitInterval, hubResolveTimeout, true, func(ctx context.Context) (bool, error) { - ke, err := hub.KnativeEventing().Get(ctx, names.KnativeEventing, metav1.GetOptions{}) + ke, err := hub.Operator.KnativeEventings(names.Namespace).Get(ctx, names.KnativeEventing, metav1.GetOptions{}) if err != nil { if apierrs.IsNotFound(err) { return false, nil @@ -173,12 +175,12 @@ func waitForSpokeEventingDeploymentsReady(ctx context.Context, t *testing.T, hub t.Fatalf("hub KnativeEventing %s/%s did not reach %s=True: %v", names.Namespace, names.KnativeEventing, base.TargetClusterResolved, resolveErr) } - waitForSpokeDeploymentsAvailable(ctx, t, spoke, names.Namespace) + waitForSpokeDeploymentsAvailable(ctx, t, spoke, spokeEventingInstallNamespace) } func assertTargetClusterResolvedEventing(ctx context.Context, t *testing.T, hub *test.Clients, names test.ResourceNames) { t.Helper() - ke, err := hub.KnativeEventing().Get(ctx, names.KnativeEventing, metav1.GetOptions{}) + ke, err := hub.Operator.KnativeEventings(names.Namespace).Get(ctx, names.KnativeEventing, metav1.GetOptions{}) if err != nil { t.Fatalf("Failed to get hub KnativeEventing %q: %v", names.KnativeEventing, err) } diff --git a/test/e2e/knativeserving_spoke_test.go b/test/e2e/knativeserving_spoke_test.go index 6aedb192f7..d8f7a8858c 100644 --- a/test/e2e/knativeserving_spoke_test.go +++ b/test/e2e/knativeserving_spoke_test.go @@ -41,6 +41,8 @@ import ( const ( defaultSpokeClusterProfileName = "spoke" defaultSpokeClusterProfileNamespace = "default" + spokeServingInstallNamespace = "knative-serving" + spokeEventingInstallNamespace = "knative-eventing" spokeWaitInterval = 5 * time.Second spokeReadyTimeout = 5 * time.Minute @@ -73,7 +75,7 @@ func TestMulticlusterKnativeServingSpokeDeployment(t *testing.T) { Namespace: test.ServingOperatorNamespace, } - ensureSpokeNamespace(ctx, t, spoke, names.Namespace) + ensureSpokeNamespace(ctx, t, spoke, spokeServingInstallNamespace) test.CleanupOnInterrupt(func() { test.TearDown(hub, names) }) defer test.TearDown(hub, names) @@ -96,13 +98,13 @@ func TestMulticlusterKnativeServingSpokeDeployment(t *testing.T) { if err := deleteHubKnativeServing(ctx, hub, names); err != nil { t.Fatalf("Failed to delete hub KnativeServing %q: %v", names.KnativeServing, err) } - if err := waitForSpokeDeploymentsGone(ctx, t, spoke, names.Namespace); err != nil { + if err := waitForSpokeDeploymentsGone(ctx, t, spoke, spokeServingInstallNamespace); err != nil { t.Fatalf("Spoke deployments still present after deletion in namespace %q: %v", - names.Namespace, err) + spokeServingInstallNamespace, err) } - waitForSpokeManagedServicesGone(ctx, t, spoke, names.Namespace, + waitForSpokeManagedServicesGone(ctx, t, spoke, spokeServingInstallNamespace, "app.kubernetes.io/name=knative-serving") - assertAnchorConfigMapGone(ctx, t, spoke, names.Namespace, + assertAnchorConfigMapGone(ctx, t, spoke, spokeServingInstallNamespace, "knativeserving-"+names.KnativeServing+"-root-owner") }) } @@ -131,7 +133,7 @@ func createKnativeServingWithSpokeRef(ctx context.Context, clients *test.Clients }, }, } - _, err := clients.KnativeServing().Create(ctx, ks, metav1.CreateOptions{}) + _, err := clients.Operator.KnativeServings(names.Namespace).Create(ctx, ks, metav1.CreateOptions{}) if apierrs.IsAlreadyExists(err) { return nil } @@ -139,7 +141,7 @@ func createKnativeServingWithSpokeRef(ctx context.Context, clients *test.Clients } func deleteHubKnativeServing(ctx context.Context, clients *test.Clients, names test.ResourceNames) error { - if err := clients.KnativeServing().Delete(ctx, names.KnativeServing, metav1.DeleteOptions{}); err != nil { + if err := clients.Operator.KnativeServings(names.Namespace).Delete(ctx, names.KnativeServing, metav1.DeleteOptions{}); err != nil { if apierrs.IsNotFound(err) { return nil } @@ -147,7 +149,7 @@ func deleteHubKnativeServing(ctx context.Context, clients *test.Clients, names t } return wait.PollUntilContextTimeout(ctx, spokeWaitInterval, spokeGoneTimeout, true, func(ctx context.Context) (bool, error) { - _, err := clients.KnativeServing().Get(ctx, names.KnativeServing, metav1.GetOptions{}) + _, err := clients.Operator.KnativeServings(names.Namespace).Get(ctx, names.KnativeServing, metav1.GetOptions{}) if apierrs.IsNotFound(err) { return true, nil } @@ -176,7 +178,7 @@ func waitForSpokeDeploymentsReady(ctx context.Context, t *testing.T, hub *test.C var lastResolveStatus string resolveErr := wait.PollUntilContextTimeout(ctx, spokeWaitInterval, hubResolveTimeout, true, func(ctx context.Context) (bool, error) { - ks, err := hub.KnativeServing().Get(ctx, names.KnativeServing, metav1.GetOptions{}) + ks, err := hub.Operator.KnativeServings(names.Namespace).Get(ctx, names.KnativeServing, metav1.GetOptions{}) if err != nil { if apierrs.IsNotFound(err) { return false, nil @@ -211,12 +213,12 @@ func waitForSpokeDeploymentsReady(ctx context.Context, t *testing.T, hub *test.C t.Fatalf("hub KnativeServing %s/%s did not reach %s=True: %v", names.Namespace, names.KnativeServing, base.TargetClusterResolved, resolveErr) } - waitForSpokeDeploymentsAvailable(ctx, t, spoke, names.Namespace) + waitForSpokeDeploymentsAvailable(ctx, t, spoke, spokeServingInstallNamespace) } func assertTargetClusterResolved(ctx context.Context, t *testing.T, hub *test.Clients, names test.ResourceNames) { t.Helper() - ks, err := hub.KnativeServing().Get(ctx, names.KnativeServing, metav1.GetOptions{}) + ks, err := hub.Operator.KnativeServings(names.Namespace).Get(ctx, names.KnativeServing, metav1.GetOptions{}) if err != nil { t.Fatalf("Failed to get hub KnativeServing %q: %v", names.KnativeServing, err) } diff --git a/test/resources/verify.go b/test/resources/verify.go index 2291840057..281cc0ef27 100644 --- a/test/resources/verify.go +++ b/test/resources/verify.go @@ -38,7 +38,7 @@ import ( // AssertKSOperatorCRReadyStatus verifies if the KnativeServing reaches the READY status. func AssertKSOperatorCRReadyStatus(t *testing.T, clients *test.Clients, names test.ResourceNames) { - if _, err := WaitForKnativeServingState(clients.KnativeServing(), names.KnativeServing, + if _, err := WaitForKnativeServingState(clients.Operator.KnativeServings(names.Namespace), names.KnativeServing, IsKnativeServingReady); err != nil { t.Fatalf("KnativeService %q failed to get to the READY status: %v", names.KnativeServing, err) } @@ -319,7 +319,7 @@ func AssertKnativeDeploymentStatus(t *testing.T, clients *test.Clients, namespac // AssertKEOperatorCRReadyStatus verifies if the KnativeEventing can reach the READY status. func AssertKEOperatorCRReadyStatus(t *testing.T, clients *test.Clients, names test.ResourceNames) { - if _, err := WaitForKnativeEventingState(clients.KnativeEventing(), names.KnativeEventing, + if _, err := WaitForKnativeEventingState(clients.Operator.KnativeEventings(names.Namespace), names.KnativeEventing, IsKnativeEventingReady); err != nil { t.Fatalf("KnativeService %q failed to get to the READY status: %v", names.KnativeEventing, err) } From e31a55dc646efecca006455d02220d3ef010f65b Mon Sep 17 00:00:00 2001 From: kahirokunn Date: Thu, 27 Aug 2026 21:40:41 +0900 Subject: [PATCH 2/2] Mark multi-cluster deployment as alpha Signed-off-by: kahirokunn --- docs/multicluster.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/multicluster.md b/docs/multicluster.md index 67a9bad9cd..1e151c8484 100644 --- a/docs/multicluster.md +++ b/docs/multicluster.md @@ -1,11 +1,14 @@ # Multi-Cluster Deployment +**Stage**: Alpha, disabled by default + The operator can deploy Knative Serving and Eventing to remote clusters from a single hub cluster. Platform administrators keep the management CRs on the hub and use `spec.clusterProfileRef` to choose the spoke cluster. The hub needs network access to each spoke API server. Connection details are -resolved through the Cluster Inventory API (`ClusterProfile`). +resolved through the Cluster Inventory API `v1alpha1` (`ClusterProfile`), so its +schema and access-provider integration may change between releases. > Note: if direct connectivity is not available, reverse the direction with > [OCM Cluster Proxy](https://open-cluster-management.io/docs/getting-started/integration/cluster-proxy/).