Skip to content

Commit 5939162

Browse files
authored
PCP-5152: Multiple node HCP cluster losing interface reference (#243)
* PCP-5152: Multiple node HCP cluster losing interface reference * Additional changes (#225) * Additional changes * trust password and code cleanup (#226) * Additional changes * Add gate to registration logic, seperate it to initializer and put grace period. Code cleanup. * Add gate to registration logic, seperate it to initializer and put grace period. Code cleanup. (#234) * Additional changes * Add gate to registration logic, seperate it to initializer and put grace period. Code cleanup. * - Gosec fix - Cleanup daemonset once all host are registered * Additional changes * Use hsotname for lxd registration. Code refactoring. (#240) * Use hsotname for lxd registration. Code refactoring. * merge conflict
1 parent 18b8b48 commit 5939162

File tree

14 files changed

+919
-313
lines changed

14 files changed

+919
-313
lines changed

controllers/lxd_initializer_ds.go

Lines changed: 237 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)