@@ -6,12 +6,17 @@ import (
66 "strings"
77
88 appsv1 "k8s.io/api/apps/v1"
9+ corev1 "k8s.io/api/core/v1"
910 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
11+ "k8s.io/apimachinery/pkg/labels"
12+ "k8s.io/apimachinery/pkg/runtime/schema"
1013 "sigs.k8s.io/controller-runtime/pkg/client"
1114 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
1215 "sigs.k8s.io/yaml"
1316
1417 "github.com/spectrocloud/cluster-api-provider-maas/pkg/maas/scope"
18+ "github.com/spectrocloud/cluster-api-provider-maas/pkg/util"
19+ "github.com/spectrocloud/cluster-api-provider-maas/pkg/util/trust"
1520
1621 // embed template
1722 _ "embed"
@@ -38,38 +43,251 @@ func (r *MaasClusterReconciler) ensureLXDInitializerDS(ctx context.Context, clus
3843 dsNamespace := cluster .Namespace
3944
4045 // Always operate on the TARGET cluster client
46+ remoteClient , err := r .getTargetClient (ctx , clusterScope )
47+ if err != nil {
48+ return err
49+ }
50+
51+ // If feature is off or cluster is being deleted, we're done
52+ if ! clusterScope .IsLXDHostEnabled () || ! cluster .ObjectMeta .DeletionTimestamp .IsZero () {
53+ return nil
54+ }
55+
56+ // Gate: ensure pivot completed. Require mgmt namespace to have clusterEnv=target
57+ isTarget , clusterEnv := r .namespaceIsTarget (ctx , dsNamespace )
58+ if ! isTarget {
59+ r .Log .Info ("Namespace not marked as target; deferring LXD initializer" , "namespace" , dsNamespace , "clusterEnv" , clusterEnv )
60+ return nil
61+ }
62+
63+ // Gate: derive desired CP count from MaasCloudConfig; fallback to KCP
64+ desiredCP , readyByKCP := r .computeDesiredControlPlane (ctx , dsNamespace , cluster .Name )
65+
66+ if ok := r .enoughCPNodesReady (ctx , remoteClient , desiredCP , readyByKCP ); ! ok {
67+ return nil
68+ }
69+
70+ if err := r .deleteExistingInitializerDS (ctx , remoteClient , dsNamespace ); err != nil {
71+ return err
72+ }
73+
74+ // Ensure RBAC resources are created on target cluster
75+ if err := r .ensureLXDInitializerRBACOnTarget (ctx , remoteClient , dsNamespace ); err != nil {
76+ return fmt .Errorf ("failed to ensure LXD initializer RBAC: %v" , err )
77+ }
78+
79+ if done , err := r .maybeShortCircuitDelete (ctx , remoteClient , dsNamespace , desiredCP , dsName ); err != nil {
80+ return err
81+ } else if done {
82+ return nil
83+ }
84+
85+ ds , err := r .renderDaemonSetForCluster (clusterScope , dsName , dsNamespace )
86+ if err != nil {
87+ return err
88+ }
89+
90+ // Do not set owner refs across clusters; just create/patch on target cluster
91+ _ , err = controllerutil .CreateOrPatch (ctx , remoteClient , ds , func () error { return nil })
92+ return err
93+ }
94+
95+ // ensureLXDInitializerRBACOnTarget creates the RBAC resources for lxd-initializer on the target cluster
96+ func (r * MaasClusterReconciler ) ensureLXDInitializerRBACOnTarget (ctx context.Context , remoteClient client.Client , namespace string ) error {
97+ // Parse RBAC template into separate resources
98+ rbacYaml := strings .ReplaceAll (lxdInitRBACTemplate , "namespace: default" , fmt .Sprintf ("namespace: %s" , namespace ))
99+
100+ // Split the YAML into separate documents
101+ docs := strings .Split (rbacYaml , "---" )
102+
103+ for _ , doc := range docs {
104+ doc = strings .TrimSpace (doc )
105+ if doc == "" {
106+ continue
107+ }
108+
109+ // Parse as unstructured object to handle different resource types
110+ obj := & unstructured.Unstructured {}
111+ if err := yaml .Unmarshal ([]byte (doc ), obj ); err != nil {
112+ return fmt .Errorf ("failed to unmarshal RBAC resource: %v" , err )
113+ }
114+
115+ // Set namespace for namespaced resources
116+ if obj .GetKind () == "ServiceAccount" {
117+ obj .SetNamespace (namespace )
118+ }
119+
120+ // Create or update the resource on target cluster
121+ _ , err := controllerutil .CreateOrPatch (ctx , remoteClient , obj , func () error { return nil })
122+ if err != nil {
123+ return fmt .Errorf ("failed to create/patch %s %s: %v" , obj .GetKind (), obj .GetName (), err )
124+ }
125+ }
126+
127+ return nil
128+ }
129+
130+ // getTargetClient returns the workload cluster client or a wrapped error
131+ func (r * MaasClusterReconciler ) getTargetClient (ctx context.Context , clusterScope * scope.ClusterScope ) (client.Client , error ) {
41132 remoteClient , err := clusterScope .GetWorkloadClusterClient (ctx )
42133 if err != nil {
43- return fmt .Errorf ("failed to get target cluster client: %v" , err )
134+ return nil , fmt .Errorf ("failed to get target cluster client: %v" , err )
44135 }
136+ return remoteClient , nil
137+ }
45138
46- // First clean up any existing DaemonSets in this namespace
139+ // namespaceIsTarget checks if the management namespace is annotated as clusterEnv=target
140+ func (r * MaasClusterReconciler ) namespaceIsTarget (ctx context.Context , namespace string ) (bool , string ) {
141+ mgmtNS := & corev1.Namespace {}
142+ if err := r .Client .Get (ctx , client.ObjectKey {Name : namespace }, mgmtNS ); err != nil {
143+ return false , ""
144+ }
145+ if mgmtNS .Annotations == nil {
146+ return false , ""
147+ }
148+ v := strings .TrimSpace (mgmtNS .Annotations ["clusterEnv" ])
149+ return v == "target" , v
150+ }
151+
152+ // computeDesiredControlPlane determines desired control-plane replicas and ready count
153+ func (r * MaasClusterReconciler ) computeDesiredControlPlane (ctx context.Context , namespace , clusterName string ) (int32 , int32 ) {
154+ desiredCP := int32 (1 )
155+ readyByKCP := int32 (0 )
156+
157+ // Prefer MaasCloudConfig.spec.machinePoolConfig[].size where isControlPlane=true (sum)
158+ mccList := & unstructured.UnstructuredList {}
159+ mccList .SetGroupVersionKind (schema.GroupVersionKind {Group : "cluster.spectrocloud.com" , Version : "v1alpha1" , Kind : "MaasCloudConfigList" })
160+ if err := r .Client .List (ctx , mccList , client .InNamespace (namespace )); err == nil {
161+ var sum int64
162+ for _ , it := range mccList .Items {
163+ owned := false
164+ for _ , or := range it .GetOwnerReferences () {
165+ if or .Name == clusterName {
166+ owned = true
167+ break
168+ }
169+ }
170+ if ! owned && ! strings .HasSuffix (it .GetName (), "-maas-config" ) {
171+ continue
172+ }
173+ pools , found , _ := unstructured .NestedSlice (it .Object , "spec" , "machinePoolConfig" )
174+ if ! found {
175+ continue
176+ }
177+ for _ , p := range pools {
178+ if mp , ok := p .(map [string ]interface {}); ok {
179+ isCP , _ , _ := unstructured .NestedBool (mp , "isControlPlane" )
180+ if ! isCP {
181+ continue
182+ }
183+ if v , foundSz , _ := unstructured .NestedInt64 (mp , "size" ); foundSz && v > 0 {
184+ sum += v
185+ }
186+ }
187+ }
188+ if sum > 0 {
189+ desiredCP = util .SafeInt64ToInt32 (sum )
190+ break
191+ }
192+ }
193+ }
194+
195+ // Fallback: use KCP if MCC not found
196+ if desiredCP == 1 {
197+ kcpList := & unstructured.UnstructuredList {}
198+ kcpList .SetGroupVersionKind (schema.GroupVersionKind {Group : "controlplane.cluster.x-k8s.io" , Version : "v1beta1" , Kind : "KubeadmControlPlaneList" })
199+ if err := r .Client .List (ctx , kcpList , client .InNamespace (namespace ), client.MatchingLabels {
200+ "cluster.x-k8s.io/cluster-name" : clusterName ,
201+ }); err == nil {
202+ if len (kcpList .Items ) > 0 {
203+ item := kcpList .Items [0 ]
204+ if v , found , _ := unstructured .NestedInt64 (item .Object , "spec" , "replicas" ); found && v > 0 {
205+ desiredCP = util .SafeInt64ToInt32 (v )
206+ }
207+ if v , found , _ := unstructured .NestedInt64 (item .Object , "status" , "readyReplicas" ); found && v >= 0 {
208+ readyByKCP = util .SafeInt64ToInt32 (v )
209+ }
210+ }
211+ }
212+ }
213+
214+ return desiredCP , readyByKCP
215+ }
216+
217+ // enoughCPNodesReady checks the target cluster for Ready control-plane nodes
218+ func (r * MaasClusterReconciler ) enoughCPNodesReady (ctx context.Context , remoteClient client.Client , desiredCP , readyByKCP int32 ) bool {
219+ nodeList := & corev1.NodeList {}
220+ cpSelector := labels .SelectorFromSet (labels.Set {
221+ "node-role.kubernetes.io/control-plane" : "" ,
222+ })
223+ if err := remoteClient .List (ctx , nodeList , & client.ListOptions {LabelSelector : cpSelector }); err == nil {
224+ ready := 0
225+ for _ , n := range nodeList .Items {
226+ for _ , c := range n .Status .Conditions {
227+ if c .Type == corev1 .NodeReady && c .Status == corev1 .ConditionTrue {
228+ ready ++
229+ break
230+ }
231+ }
232+ }
233+ if int64 (len (nodeList .Items )) < int64 (desiredCP ) || int64 (ready ) < int64 (desiredCP ) {
234+ r .Log .Info ("Not enough control-plane nodes present/ready yet; skipping DS for now" , "desiredCP" , desiredCP , "readyByKCP" , readyByKCP , "nodeList" , len (nodeList .Items ), "ready" , ready )
235+ return false
236+ }
237+ }
238+ return true
239+ }
240+
241+ // deleteExistingInitializerDS removes any DaemonSets with old labeling in the namespace
242+ func (r * MaasClusterReconciler ) deleteExistingInitializerDS (ctx context.Context , remoteClient client.Client , namespace string ) error {
47243 dsList := & appsv1.DaemonSetList {}
48- if err := remoteClient .List (ctx , dsList , client .InNamespace (dsNamespace ), client.MatchingLabels {
244+ if err := remoteClient .List (ctx , dsList , client .InNamespace (namespace ), client.MatchingLabels {
49245 "app" : "lxd-initializer" ,
50246 }); err != nil {
51247 return fmt .Errorf ("failed to list DaemonSets: %v" , err )
52248 }
53249
54- // Delete all existing LXD initializer DaemonSets
55250 for _ , ds := range dsList .Items {
56251 if err := remoteClient .Delete (ctx , & ds ); err != nil {
57252 return fmt .Errorf ("failed to delete DaemonSet %s: %v" , ds .Name , err )
58253 }
59254 }
255+ return nil
256+ }
60257
61- // If feature is off or cluster is being deleted, we're done after cleanup
62- if ! clusterScope .IsLXDHostEnabled () || ! cluster .ObjectMeta .DeletionTimestamp .IsZero () {
63- return nil
258+ // maybeShortCircuitDelete deletes the DS if all CP nodes are already initialized
259+ func (r * MaasClusterReconciler ) maybeShortCircuitDelete (ctx context.Context , remoteClient client.Client , namespace string , desiredCP int32 , dsName string ) (bool , error ) {
260+ shortCircuitNodes := & corev1.NodeList {}
261+ shortCircuitSelector := labels .SelectorFromSet (labels.Set {
262+ "node-role.kubernetes.io/control-plane" : "" ,
263+ })
264+ if err := remoteClient .List (ctx , shortCircuitNodes , & client.ListOptions {LabelSelector : shortCircuitSelector }); err != nil || len (shortCircuitNodes .Items ) == 0 {
265+ return false , nil
64266 }
65267
66- // Ensure RBAC resources are created on target cluster
67- if err := r .ensureLXDInitializerRBACOnTarget (ctx , remoteClient , dsNamespace ); err != nil {
68- return fmt .Errorf ("failed to ensure LXD initializer RBAC: %v" , err )
268+ initCount := 0
269+ for _ , n := range shortCircuitNodes .Items {
270+ if n .Labels != nil && n .Labels ["lxdhost.cluster.com/initialized" ] == "true" {
271+ initCount ++
272+ }
273+ }
274+ if int64 (len (shortCircuitNodes .Items )) >= int64 (desiredCP ) && int64 (initCount ) >= int64 (desiredCP ) {
275+ shortCircuitDSList := & appsv1.DaemonSetList {}
276+ if err := remoteClient .List (ctx , shortCircuitDSList , client .InNamespace (namespace ), client.MatchingLabels {"app" : dsName }); err == nil {
277+ for _ , ds := range shortCircuitDSList .Items {
278+ _ = remoteClient .Delete (ctx , & ds )
279+ }
280+ }
281+ return true , nil
69282 }
283+ return false , nil
284+ }
70285
71- // pull LXD config
286+ // renderDaemonSetForCluster renders the DS YAML from template using cluster config and returns a DaemonSet object
287+ func (r * MaasClusterReconciler ) renderDaemonSetForCluster (clusterScope * scope.ClusterScope , dsName , namespace string ) (* appsv1.DaemonSet , error ) {
288+ cluster := clusterScope .MaasCluster
72289 cfg := clusterScope .GetLXDConfig ()
290+
73291 sb := cfg .StorageBackend
74292 if sb == "" {
75293 sb = "zfs"
@@ -85,8 +303,12 @@ func (r *MaasClusterReconciler) ensureLXDInitializerDS(ctx context.Context, clus
85303 }
86304
87305 nt := cfg .NICType
306+ if nt == "" {
307+ nt = "bridged"
308+ }
88309 np := cfg .NICParent
89- tp := "capmaas"
310+ // Deterministic per-cluster trust password derived from cluster UID
311+ tp := trust .DeriveTrustPassword (string (cluster .UID ))
90312
91313 rendered := render (map [string ]string {
92314 "${STORAGE_BACKEND}" : sb ,
@@ -102,7 +324,7 @@ func (r *MaasClusterReconciler) ensureLXDInitializerDS(ctx context.Context, clus
102324
103325 ds := & appsv1.DaemonSet {}
104326 if err := yaml .Unmarshal ([]byte (dsYaml ), ds ); err != nil {
105- return err
327+ return nil , err
106328 }
107329
108330 // ensure names/labels are cluster-specific without touching the image name
@@ -113,44 +335,7 @@ func (r *MaasClusterReconciler) ensureLXDInitializerDS(ctx context.Context, clus
113335 ds .Labels ["app" ] = dsName
114336 ds .Spec .Selector .MatchLabels ["app" ] = dsName
115337 ds .Spec .Template .Labels ["app" ] = dsName
116- ds .Namespace = dsNamespace
338+ ds .Namespace = namespace
117339
118- // Do not set owner refs across clusters; just create/patch on target cluster
119- _ , err = controllerutil .CreateOrPatch (ctx , remoteClient , ds , func () error { return nil })
120- return err
121- }
122-
123- // ensureLXDInitializerRBACOnTarget creates the RBAC resources for lxd-initializer on the target cluster
124- func (r * MaasClusterReconciler ) ensureLXDInitializerRBACOnTarget (ctx context.Context , remoteClient client.Client , namespace string ) error {
125- // Parse RBAC template into separate resources
126- rbacYaml := strings .ReplaceAll (lxdInitRBACTemplate , "namespace: default" , fmt .Sprintf ("namespace: %s" , namespace ))
127-
128- // Split the YAML into separate documents
129- docs := strings .Split (rbacYaml , "---" )
130-
131- for _ , doc := range docs {
132- doc = strings .TrimSpace (doc )
133- if doc == "" {
134- continue
135- }
136-
137- // Parse as unstructured object to handle different resource types
138- obj := & unstructured.Unstructured {}
139- if err := yaml .Unmarshal ([]byte (doc ), obj ); err != nil {
140- return fmt .Errorf ("failed to unmarshal RBAC resource: %v" , err )
141- }
142-
143- // Set namespace for namespaced resources
144- if obj .GetKind () == "ServiceAccount" {
145- obj .SetNamespace (namespace )
146- }
147-
148- // Create or update the resource on target cluster
149- _ , err := controllerutil .CreateOrPatch (ctx , remoteClient , obj , func () error { return nil })
150- if err != nil {
151- return fmt .Errorf ("failed to create/patch %s %s: %v" , obj .GetKind (), obj .GetName (), err )
152- }
153- }
154-
155- return nil
340+ return ds , nil
156341}
0 commit comments