Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ metadata:
spec:
controlPlaneRef:
apiVersion: controlplane.cluster.x-k8s.io/v1beta1
kind: RKE2ControlPlane
kind: KThreesControlPlane
name: sample-control-plane
namespace: default

13 changes: 0 additions & 13 deletions config/samples/rke2_v1beta1_control_plane.yaml

This file was deleted.

10 changes: 10 additions & 0 deletions deployment/charts/cluster-connect-gateway/templates/rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ rules:
- patch
- update
- watch
- apiGroups:
- controlplane.cluster.x-k8s.io
resources:
- kthreescontrolplanes
verbs:
- get
- list
- patch
- update
- watch
- apiGroups:
- ""
resources:
Expand Down
153 changes: 145 additions & 8 deletions internal/controller/clusterconnect_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/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"
kerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/client-go/tools/record"
Expand Down Expand Up @@ -150,7 +151,7 @@ func (r *ClusterConnectReconciler) SetupWithManager(ctx context.Context, mgr ctr
return errors.Wrap(err, "failed to initialize token manager")
}

// Initialize provider manager with RKE2ControlPlane provider.
// Initialize provider manager with KThreesControlPlane provider.
// Add KubeadmControlPlane when implemented.
r.providerManager = provider.NewProviderManager().
WithProvider("RKE2ControlPlane", "/var/lib/rancher/rke2/agent/pod-manifests/connect-agent.yaml").
Expand Down Expand Up @@ -306,18 +307,37 @@ func (r *ClusterConnectReconciler) reconcile(ctx context.Context, cc *v1alpha1.C
// 3) Initialize the connection probe state
// 4) Set control plane endpoint
// 5) Set the connect-agent config to Cluster object
// 6) Wait until the Cluster object update is reconciled by Topology controller
// 6) Wait until the Cluster object update is reconciled by Topology controller (skip for legacy mode)
// 7) Update kubeconfig secret

// Check if we're in legacy mode to determine which phases to run
isLegacyMode := false
if cc.Spec.ClusterRef != nil {
cluster := &clusterv1.Cluster{}
clusterKey := client.ObjectKey{
Namespace: cc.Spec.ClusterRef.Namespace,
Name: cc.Spec.ClusterRef.Name,
}
if err := r.Client.Get(ctx, clusterKey, cluster); err == nil {
isLegacyMode = cluster.Spec.Topology == nil
}
}

phases := []func(context.Context, *v1alpha1.ClusterConnect) error{
r.reconcileAuthToken,
r.reconcileConnectAgentManifest,
r.reconcileConnectionProbe,
r.reconcileControlPlaneEndpoint,
r.reconcileClusterSpec,
r.reconcileTopology,
r.reconcileKubeconfig,
}

// Only add reconcileTopology for topology mode clusters
if !isLegacyMode {
phases = append(phases, r.reconcileTopology)
}

phases = append(phases, r.reconcileKubeconfig)

errs := []error{}
for _, phase := range phases {
if err := phase(ctx, cc); err != nil {
Expand Down Expand Up @@ -396,7 +416,7 @@ func (r *ClusterConnectReconciler) reconcileControlPlaneEndpoint(ctx context.Con
}

func (r *ClusterConnectReconciler) reconcileClusterSpec(ctx context.Context, cc *v1alpha1.ClusterConnect) error {
_ = log.FromContext(ctx)
log := log.FromContext(ctx)

// Return early if ClusterRef is not set in the ClusterConnect object.
if cc.Spec.ClusterRef == nil {
Expand All @@ -415,10 +435,10 @@ func (r *ClusterConnectReconciler) reconcileClusterSpec(ctx context.Context, cc
return fmt.Errorf("failed to get Cluster object %s/%s: %v", clusterKey.Namespace, clusterKey.Name, err)
}

// Validate Cluster topology.
// Handle legacy mode by patching ControlPlane directly
if cluster.Spec.Topology == nil {
setClusterSpecUpdatedConditionFalse(cc)
return fmt.Errorf("cluster %s/%s has no topology defined", clusterKey.Namespace, clusterKey.Name)
log.Info("Cluster is using legacy mode without topology. Will inject directly into ControlPlane object.", "cluster", clusterKey)
return r.reconcileLegacyMode(ctx, cc, cluster)
}

// Prepare the agent configuration.
Expand Down Expand Up @@ -610,6 +630,123 @@ func (r *ClusterConnectReconciler) reconcileKubeconfig(ctx context.Context, cc *
return nil
}

func (r *ClusterConnectReconciler) reconcileLegacyMode(ctx context.Context, cc *v1alpha1.ClusterConnect, cluster *clusterv1.Cluster) error {
log := log.FromContext(ctx)

// For legacy mode, we need to directly patch the ControlPlane object
if cluster.Spec.ControlPlaneRef == nil {
return fmt.Errorf("cluster has no ControlPlaneRef")
}

// Get the ControlPlane object
controlPlaneRef := cluster.Spec.ControlPlaneRef
controlPlaneKey := client.ObjectKey{
Namespace: controlPlaneRef.Namespace,
Name: controlPlaneRef.Name,
}

// Use unstructured object to handle any ControlPlane type
controlPlane := &unstructured.Unstructured{}
controlPlane.SetAPIVersion(controlPlaneRef.APIVersion)
controlPlane.SetKind(controlPlaneRef.Kind)

if err := r.Client.Get(ctx, controlPlaneKey, controlPlane); err != nil {
setClusterSpecUpdatedConditionFalse(cc)
return fmt.Errorf("failed to get ControlPlane object %s/%s: %v", controlPlaneKey.Namespace, controlPlaneKey.Name, err)
}

// Prepare the agent configuration file
agentFile := map[string]interface{}{
"path": agentManifestPath,
"owner": "root:root",
"content": cc.Status.AgentManifest,
}

// Get existing files from the correct path based on control plane kind
spec, exists := controlPlane.Object["spec"].(map[string]interface{})
if !exists {
spec = make(map[string]interface{})
controlPlane.Object["spec"] = spec
}

var files []interface{}
var filesPath []string

// Determine the correct path for files based on the control plane kind
switch controlPlane.GetKind() {
case "KThreesControlPlane":
// For KThreesControlPlane, files are at spec.kthreesConfigSpec.files
filesPath = []string{"kthreesConfigSpec", "files"}
case "RKE2ControlPlane":
// For RKE2ControlPlane, files are at spec.files
filesPath = []string{"files"}
default:
// Default to spec.files for other providers
filesPath = []string{"files"}
}

// Navigate to the correct nested path
current := spec
for _, pathSegment := range filesPath[:len(filesPath)-1] {
if next, exists := current[pathSegment].(map[string]interface{}); exists {
current = next
} else {
// Create missing intermediate objects
current[pathSegment] = make(map[string]interface{})
current = current[pathSegment].(map[string]interface{})
}
}

// Get the files array from the final path segment
finalKey := filesPath[len(filesPath)-1]
if existingFiles, exists := current[finalKey].([]interface{}); exists {
files = existingFiles
} else {
files = []interface{}{}
}

// Check if connect-agent.yaml already exists
agentFileExists := false
for i, file := range files {
if fileMap, ok := file.(map[string]interface{}); ok {
if path, ok := fileMap["path"].(string); ok && path == agentManifestPath {
// Update existing file if content differs
if content, ok := fileMap["content"].(string); !ok || content != cc.Status.AgentManifest {
files[i] = agentFile
log.Info("Updated connect-agent.yaml file in ControlPlane", "controlPlane", controlPlaneKey)
} else {
log.Info("connect-agent.yaml file already up to date in ControlPlane", "controlPlane", controlPlaneKey)
setClusterSpecReadyConditionTrue(cc)
setTopologyReconciledConditionTrue(cc)
return nil
}
agentFileExists = true
break
}
}
}

// Add the file if it doesn't exist
if !agentFileExists {
files = append(files, agentFile)
log.Info("Added connect-agent.yaml file to ControlPlane", "controlPlane", controlPlaneKey)
}

// Update the files in the ControlPlane object at the correct path
current[finalKey] = files

// Update the ControlPlane object directly
if err := r.Client.Update(ctx, controlPlane); err != nil {
setClusterSpecUpdatedConditionFalse(cc)
return fmt.Errorf("failed to update ControlPlane object %s/%s: %v", controlPlaneKey.Namespace, controlPlaneKey.Name, err)
}

log.Info("Successfully injected connect-agent configuration into ControlPlane", "controlPlane", controlPlaneKey)
setClusterSpecReadyConditionTrue(cc)
setTopologyReconciledConditionTrue(cc)
return nil
}

func (r *ClusterConnectReconciler) reconcileConnectionProbe(ctx context.Context, cc *v1alpha1.ClusterConnect) error {
log.FromContext(ctx)
// Initialize ConnectionProbe if not already set.
Expand Down
2 changes: 1 addition & 1 deletion internal/controller/clusterconnect_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ var _ = Describe("ClusterConnect Controller", Ordered, func() {
Spec: clusterv1.ClusterSpec{
ControlPlaneRef: &corev1.ObjectReference{
APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
Kind: "RKE2ControlPlane",
Kind: "KThreesControlPlane",
Name: testName,
Namespace: "default",
},
Expand Down
2 changes: 1 addition & 1 deletion internal/controller/conditions.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func setControlPlaneEndpointSetConditionTrue(cc *v1alpha1.ClusterConnect, messag
})
}

func setClusterSpecReadyConditionTrue(cc *v1alpha1.ClusterConnect, message ...string) {
func setClusterSpecReadyConditionTrue(cc *v1alpha1.ClusterConnect, message ...string) { //nolint:unparam
conditionMessage := ""
if len(message) > 0 {
conditionMessage = message[0]
Expand Down
Loading