diff --git a/api/v1beta1/cluster_types.go b/api/v1beta1/cluster_types.go index c07db3b89592..c9b2a5656a96 100644 --- a/api/v1beta1/cluster_types.go +++ b/api/v1beta1/cluster_types.go @@ -332,6 +332,11 @@ type Topology struct { // The name of the ClusterClass object to create the topology. Class string `json:"class"` + // The namespace of the ClusterClass object to create the topology. + // + // +optional + ClassNamespace string `json:"classNamespace,omitempty"` + // The Kubernetes version of the cluster. Version string `json:"version"` @@ -860,7 +865,17 @@ func (c *Cluster) GetClassKey() types.NamespacedName { if c.Spec.Topology == nil { return types.NamespacedName{} } - return types.NamespacedName{Namespace: c.GetNamespace(), Name: c.Spec.Topology.Class} + + return types.NamespacedName{Namespace: c.GetInfrastructureNamespace(), Name: c.Spec.Topology.Class} +} + +// GetInfrastructureNamespace returns common namespace for the cluster infrastructure. +func (c *Cluster) GetInfrastructureNamespace() string { + if c.Spec.Topology == nil || c.Spec.Topology.ClassNamespace == "" { + return c.Namespace + } + + return c.Spec.Topology.ClassNamespace } // GetConditions returns the set of conditions for this object. diff --git a/api/v1beta1/zz_generated.openapi.go b/api/v1beta1/zz_generated.openapi.go index 23f3ecfd8d9c..0f9c9cef455a 100644 --- a/api/v1beta1/zz_generated.openapi.go +++ b/api/v1beta1/zz_generated.openapi.go @@ -4461,6 +4461,13 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_Topology(ref common.ReferenceCallb Format: "", }, }, + "classNamespace": { + SchemaProps: spec.SchemaProps{ + Description: "The namespace of the ClusterClass object to create the topology.", + Type: []string{"string"}, + Format: "", + }, + }, "version": { SchemaProps: spec.SchemaProps{ Description: "The Kubernetes version of the cluster.", diff --git a/config/crd/bases/cluster.x-k8s.io_clusters.yaml b/config/crd/bases/cluster.x-k8s.io_clusters.yaml index 4ad16356145b..0deba724fe95 100644 --- a/config/crd/bases/cluster.x-k8s.io_clusters.yaml +++ b/config/crd/bases/cluster.x-k8s.io_clusters.yaml @@ -928,6 +928,10 @@ spec: description: The name of the ClusterClass object to create the topology. type: string + classNamespace: + description: The namespace of the ClusterClass object to create + the topology. + type: string controlPlane: description: controlPlane describes the cluster control plane. properties: diff --git a/docs/book/src/tasks/experimental-features/cluster-class/write-clusterclass.md b/docs/book/src/tasks/experimental-features/cluster-class/write-clusterclass.md index 1c722cfcf5f9..10fe2568610b 100644 --- a/docs/book/src/tasks/experimental-features/cluster-class/write-clusterclass.md +++ b/docs/book/src/tasks/experimental-features/cluster-class/write-clusterclass.md @@ -438,6 +438,89 @@ spec: template: "{{ .cluster.name }}-{{ .machinePool.topologyName }}-{{ .random }}" ``` +### Defining a custom namespace for ClusterClass object + +As a user, I may need to create a `Cluster` from a `ClusterClass` object that exists only in a different namespace. To uniquely identify the `ClusterClass`, a `NamespaceName` ref is constructed from combination of: +* `cluster.spec.topology.classNamespace` - namespace of the `ClusterClass` object. +* `cluster.spec.topology.class` - name of the `ClusterClass` object. + +Example of the `Cluster` object with the `name/namespace` reference: + +```yaml +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Cluster +metadata: + name: my-docker-cluster + namespace: default +spec: + topology: + class: docker-clusterclass-v0.1.0 + classNamespace: default + version: v1.22.4 + controlPlane: + replicas: 3 + workers: + machineDeployments: + - class: default-worker + name: md-0 + replicas: 4 + failureDomain: region +``` + + + +#### Securing cross-namespace reference to the ClusterClass + +It is often desirable to restrict free cross-namespace `ClusterClass` access for the `Cluster` object. This can be implemented by defining a [`ValidatingAdmissionPolicy`](https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/) on the `Cluster` object. + +An example of such policy may be: + +```yaml +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: "cluster-class-ref.cluster.x-k8s.io" +spec: + failurePolicy: Fail + paramKind: + apiVersion: v1 + kind: Secret + matchConstraints: + resourceRules: + - apiGroups: ["cluster.x-k8s.io"] + apiVersions: ["v1beta1"] + operations: ["CREATE", "UPDATE"] + resources: ["clusters"] + validations: + - expression: "!has(object.spec.topology.classNamespace) || object.spec.topology.classNamespace in params.data" +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: "cluster-class-ref-binding.cluster.x-k8s.io" +spec: + policyName: "cluster-class-ref.cluster.x-k8s.io" + validationActions: [Deny] + paramRef: + name: "ref-list" + namespace: "default" + parameterNotFoundAction: Deny +--- +apiVersion: v1 +kind: Secret +metadata: + name: "ref-list" + namespace: "default" +data: + default: "" +``` + ## Advanced features of ClusterClass with patches This section will explain more advanced features of ClusterClass patches. diff --git a/internal/apis/core/v1alpha4/conversion.go b/internal/apis/core/v1alpha4/conversion.go index cd69ca35d2ba..8e2ed513d13d 100644 --- a/internal/apis/core/v1alpha4/conversion.go +++ b/internal/apis/core/v1alpha4/conversion.go @@ -42,6 +42,7 @@ func (src *Cluster) ConvertTo(dstRaw conversion.Hub) error { if dst.Spec.Topology == nil { dst.Spec.Topology = &clusterv1.Topology{} } + dst.Spec.Topology.ClassNamespace = restored.Spec.Topology.ClassNamespace dst.Spec.Topology.Variables = restored.Spec.Topology.Variables dst.Spec.Topology.ControlPlane.Variables = restored.Spec.Topology.ControlPlane.Variables diff --git a/internal/apis/core/v1alpha4/zz_generated.conversion.go b/internal/apis/core/v1alpha4/zz_generated.conversion.go index 14df0dfc9112..f5c0433a653d 100644 --- a/internal/apis/core/v1alpha4/zz_generated.conversion.go +++ b/internal/apis/core/v1alpha4/zz_generated.conversion.go @@ -1757,6 +1757,7 @@ func Convert_v1alpha4_Topology_To_v1beta1_Topology(in *Topology, out *v1beta1.To func autoConvert_v1beta1_Topology_To_v1alpha4_Topology(in *v1beta1.Topology, out *Topology, s conversion.Scope) error { out.Class = in.Class + // WARNING: in.ClassNamespace requires manual conversion: does not exist in peer-type out.Version = in.Version out.RolloutAfter = (*metav1.Time)(unsafe.Pointer(in.RolloutAfter)) if err := Convert_v1beta1_ControlPlaneTopology_To_v1alpha4_ControlPlaneTopology(&in.ControlPlane, &out.ControlPlane, s); err != nil { diff --git a/internal/controllers/topology/cluster/cluster_controller.go b/internal/controllers/topology/cluster/cluster_controller.go index 11cd135b7460..716dfbe73642 100644 --- a/internal/controllers/topology/cluster/cluster_controller.go +++ b/internal/controllers/topology/cluster/cluster_controller.go @@ -362,7 +362,6 @@ func (r *Reconciler) clusterClassToCluster(ctx context.Context, o client.Object) ctx, clusterList, client.MatchingFields{index.ClusterClassNameField: clusterClass.Name}, - client.InNamespace(clusterClass.Namespace), ); err != nil { return nil } @@ -371,7 +370,9 @@ func (r *Reconciler) clusterClassToCluster(ctx context.Context, o client.Object) // create a request for each of the clusters. requests := []ctrl.Request{} for i := range clusterList.Items { - requests = append(requests, ctrl.Request{NamespacedName: util.ObjectKey(&clusterList.Items[i])}) + if clusterList.Items[i].GetInfrastructureNamespace() == clusterClass.Namespace { + requests = append(requests, ctrl.Request{NamespacedName: util.ObjectKey(&clusterList.Items[i])}) + } } return requests } diff --git a/internal/webhooks/clusterclass.go b/internal/webhooks/clusterclass.go index 65fa7a2d69cd..49bc76976212 100644 --- a/internal/webhooks/clusterclass.go +++ b/internal/webhooks/clusterclass.go @@ -380,12 +380,19 @@ func (webhook *ClusterClass) getClustersUsingClusterClass(ctx context.Context, c clusters := &clusterv1.ClusterList{} err := webhook.Client.List(ctx, clusters, client.MatchingFields{index.ClusterClassNameField: clusterClass.Name}, - client.InNamespace(clusterClass.Namespace), ) if err != nil { return nil, err } - return clusters.Items, nil + + referencedClusters := []clusterv1.Cluster{} + for _, cluster := range clusters.Items { + if cluster.GetInfrastructureNamespace() == clusterClass.Namespace { + referencedClusters = append(referencedClusters, cluster) + } + } + + return referencedClusters, nil } func getClusterClassVariablesMapWithReverseIndex(clusterClassVariables []clusterv1.ClusterClassVariable) (map[string]*clusterv1.ClusterClassVariable, map[string]int) {