diff --git a/api/v1alpha1/kamajicontrolplane_types.go b/api/v1alpha1/kamajicontrolplane_types.go index d42d8f2..74e0609 100644 --- a/api/v1alpha1/kamajicontrolplane_types.go +++ b/api/v1alpha1/kamajicontrolplane_types.go @@ -169,6 +169,7 @@ type KamajiControlPlaneFields struct { Deployment DeploymentComponent `json:"deployment,omitempty"` } +// +kubebuilder:validation:XValidation:rule="!has(oldSelf.deploymentName) || has(self.deploymentName)", message="DeploymentName is required once set" type ExternalClusterReference struct { // The Secret object containing the kubeconfig used to interact with the remote cluster that will host // the Tenant Control Plane resources generated by the Control Plane Provider. @@ -184,6 +185,9 @@ type ExternalClusterReference struct { KubeconfigSecretNamespace string `json:"kubeconfigSecretNamespace,omitempty"` // The Namespace where the resulting TenantControlPlane must be deployed to. DeploymentNamespace string `json:"deploymentNamespace"` + // The Name of the resulting TenantControlPlane. + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="DeploymentName is immutable" + DeploymentName string `json:"deploymentName,omitempty"` } // KamajiControlPlaneStatus defines the observed state of KamajiControlPlane. @@ -246,3 +250,10 @@ type KamajiControlPlaneList struct { func init() { SchemeBuilder.Register(&KamajiControlPlane{}, &KamajiControlPlaneList{}) } + +// This label is used to detect collisions when using ExternalClusterReference.DeploymentName +// It allows to keep track of the KCP owning a given TCP when the TCP name is not the default +// "kcp-" +const ( + KamajiControlPlaneUIDLabel = "kamaji.clastix.io/kamajicontrolplane-uid" +) diff --git a/config/control-plane-components.yaml b/config/control-plane-components.yaml index eb4fe56..0750a62 100644 --- a/config/control-plane-components.yaml +++ b/config/control-plane-components.yaml @@ -1524,6 +1524,13 @@ spec: When this value is nil, the Cluster API management cluster will be used as a target. The ExternalClusterReference feature gate must be enabled with one of the available flags. properties: + deploymentName: + description: |- + The Name of the resulting TenantControlPlane. + type: string + x-kubernetes-validations: + - message: deploymentName is immutable + rule: 'self == oldSelf' deploymentNamespace: description: The Namespace where the resulting TenantControlPlane must be deployed to. type: string @@ -1547,6 +1554,9 @@ spec: - kubeconfigSecretKey - kubeconfigSecretName type: object + x-kubernetes-validations: + - message: DeploymentName is required once set + rule: '!has(oldSelf.deploymentName) || has(self.deploymentName)' extraContainers: items: description: A single application container that you want to run within a pod. @@ -8244,6 +8254,13 @@ spec: When this value is nil, the Cluster API management cluster will be used as a target. The ExternalClusterReference feature gate must be enabled with one of the available flags. properties: + deploymentName: + description: |- + The Name of the resulting TenantControlPlane. + type: string + x-kubernetes-validations: + - message: deploymentName is immutable + rule: 'self == oldSelf' deploymentNamespace: description: The Namespace where the resulting TenantControlPlane must be deployed to. type: string @@ -8267,6 +8284,9 @@ spec: - kubeconfigSecretKey - kubeconfigSecretName type: object + x-kubernetes-validations: + - message: DeploymentName is required once set + rule: '!has(oldSelf.deploymentName) || has(self.deploymentName)' extraContainers: items: description: A single application container that you want to run within a pod. diff --git a/config/crd/bases/controlplane.cluster.x-k8s.io_kamajicontrolplanes.yaml b/config/crd/bases/controlplane.cluster.x-k8s.io_kamajicontrolplanes.yaml index 60174fc..83849ab 100644 --- a/config/crd/bases/controlplane.cluster.x-k8s.io_kamajicontrolplanes.yaml +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_kamajicontrolplanes.yaml @@ -1572,6 +1572,12 @@ spec: When this value is nil, the Cluster API management cluster will be used as a target. The ExternalClusterReference feature gate must be enabled with one of the available flags. properties: + deploymentName: + description: The Name of the resulting TenantControlPlane. + type: string + x-kubernetes-validations: + - message: DeploymentName is immutable + rule: self == oldSelf deploymentNamespace: description: The Namespace where the resulting TenantControlPlane must be deployed to. @@ -1597,6 +1603,9 @@ spec: - kubeconfigSecretKey - kubeconfigSecretName type: object + x-kubernetes-validations: + - message: DeploymentName is required once set + rule: '!has(oldSelf.deploymentName) || has(self.deploymentName)' extraContainers: items: description: A single application container that you want to diff --git a/config/crd/bases/controlplane.cluster.x-k8s.io_kamajicontrolplanetemplates.yaml b/config/crd/bases/controlplane.cluster.x-k8s.io_kamajicontrolplanetemplates.yaml index ff4cba4..b67f0be 100644 --- a/config/crd/bases/controlplane.cluster.x-k8s.io_kamajicontrolplanetemplates.yaml +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_kamajicontrolplanetemplates.yaml @@ -1584,6 +1584,12 @@ spec: When this value is nil, the Cluster API management cluster will be used as a target. The ExternalClusterReference feature gate must be enabled with one of the available flags. properties: + deploymentName: + description: The Name of the resulting TenantControlPlane. + type: string + x-kubernetes-validations: + - message: DeploymentName is immutable + rule: self == oldSelf deploymentNamespace: description: The Namespace where the resulting TenantControlPlane must be deployed to. @@ -1609,6 +1615,9 @@ spec: - kubeconfigSecretKey - kubeconfigSecretName type: object + x-kubernetes-validations: + - message: DeploymentName is required once set + rule: '!has(oldSelf.deploymentName) || has(self.deploymentName)' extraContainers: items: description: A single application container that you diff --git a/controllers/kamajicontrolplane_controller_tcp.go b/controllers/kamajicontrolplane_controller_tcp.go index 02022cb..44e79d5 100644 --- a/controllers/kamajicontrolplane_controller_tcp.go +++ b/controllers/kamajicontrolplane_controller_tcp.go @@ -9,20 +9,21 @@ import ( "net" "strings" + kcpv1alpha1 "github.com/clastix/cluster-api-control-plane-provider-kamaji/api/v1alpha1" + "github.com/clastix/cluster-api-control-plane-provider-kamaji/pkg/externalclusterreference" kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" "k8s.io/utils/ptr" capiv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - - kcpv1alpha1 "github.com/clastix/cluster-api-control-plane-provider-kamaji/api/v1alpha1" - "github.com/clastix/cluster-api-control-plane-provider-kamaji/pkg/externalclusterreference" ) var ErrUnsupportedCertificateSAN = errors.New("a certificate SAN must be made of host only with no port") +var ErrTCPCollision = errors.New("Collision on remote TenantControlPlanes") //+kubebuilder:rbac:groups=kamaji.clastix.io,resources=tenantcontrolplanes,verbs=get;list;watch;create;update @@ -57,6 +58,23 @@ func (r *KamajiControlPlaneReconciler) createOrUpdateTenantControlPlane(ctx cont tcp.Labels = kcp.Labels + // Check the KCP-UID label to avoid collsions if 2 clusters with the same name + // use the same namespace with externalClusterReference + // if label is not present, it will be added + if isDelegatedExternally && kcp.Spec.Deployment.ExternalClusterReference.DeploymentName != "" { + var tcpInCluster kamajiv1alpha1.TenantControlPlane + remoteClient.Get(ctx, types.NamespacedName{Namespace: tcp.Namespace, Name: tcp.Name}, &tcpInCluster) + + if val := tcpInCluster.Labels[kcpv1alpha1.KamajiControlPlaneUIDLabel]; val != "" { + if val != string(kcp.UID) { + return errors.Wrap(ErrTCPCollision, fmt.Sprintf("Collision on TenantControlPlane %s: Value of label '%s' does not match.", tcp.Name, kcpv1alpha1.KamajiControlPlaneUIDLabel)) + } + // label matches our kcp UID -> update TCP (nothing to do here) + } else { // label not present -> claim this TCP by adding it + tcp.Labels[kcpv1alpha1.KamajiControlPlaneUIDLabel] = string(kcp.UID) + } + } + if kubeconfigSecretKey := kcp.Annotations[kamajiv1alpha1.KubeconfigSecretKeyAnnotation]; kubeconfigSecretKey != "" { tcp.Annotations[kamajiv1alpha1.KubeconfigSecretKeyAnnotation] = kubeconfigSecretKey } else { diff --git a/controllers/kamajicontrolplane_finalizer.go b/controllers/kamajicontrolplane_finalizer.go index 4125e31..1f08954 100644 --- a/controllers/kamajicontrolplane_finalizer.go +++ b/controllers/kamajicontrolplane_finalizer.go @@ -58,6 +58,23 @@ func (r *KamajiControlPlaneReconciler) handleDeletion(ctx context.Context, kcp v var tcp kamajiv1alpha1.TenantControlPlane tcp.Name, tcp.Namespace = externalclusterreference.GenerateRemoteTenantControlPlaneNames(kcp) + // Check KamajiControlPlaneUIDLabel on TCP, to avoid deleting it if it doesn't belong to our KCP + if kcp.Spec.Deployment.ExternalClusterReference.DeploymentName != "" { + + if err := remoteClient.Get(ctx, types.NamespacedName{Namespace: tcp.Namespace, Name: tcp.Name}, &tcp); err != nil { + if errors.IsNotFound(err) { + log.Info("resource may have been deleted") + } + log.Error(err, "unable to get remote TenantControlPlane") + } + + if val := tcp.GetLabels()[v1alpha1.KamajiControlPlaneUIDLabel]; val != "" && val != string(kcp.UID) { + log.Info("Did not delete remote TenantControlPlane as it belongs to another KamajiControlPlane") + + return nil + } + } + if tcpErr := remoteClient.Delete(ctx, &tcp); tcpErr != nil { if errors.IsNotFound(tcpErr) { log.Info("remote TenantControlPlane is already deleted") diff --git a/pkg/externalclusterreference/name_generator.go b/pkg/externalclusterreference/name_generator.go index 4694489..b36bbf2 100644 --- a/pkg/externalclusterreference/name_generator.go +++ b/pkg/externalclusterreference/name_generator.go @@ -25,6 +25,10 @@ func ParseKamajiControlPlaneUIDFromTenantControlPlane(tcp kamajiv1alpha1.TenantC } func GenerateRemoteTenantControlPlaneNames(kcp v1alpha1.KamajiControlPlane) (name string, namespace string) { //nolint:nonamedreturns + if kcp.Spec.Deployment.ExternalClusterReference.DeploymentName != "" { + return kcp.Spec.Deployment.ExternalClusterReference.DeploymentName, kcp.Spec.Deployment.ExternalClusterReference.DeploymentNamespace + } + return RemoteTCPPrefix + string(kcp.UID), kcp.Spec.Deployment.ExternalClusterReference.DeploymentNamespace }