Skip to content

Commit 3604ae9

Browse files
committed
GardenerNodeLifecycle: Base it on the Hypervisor CRD, do not label node
The eviction will now be triggered by the maintenance-operator, so drop setting the label. Watch the conditions on the hypervisor and manage the pods / deployments in kube-sytem to signal gardener that - the node is ready when the onboarding condition is completed - the node can be drained (and therefor terminated) if it has been evicted
1 parent 1b57ee1 commit 3604ae9

File tree

5 files changed

+124
-103
lines changed

5 files changed

+124
-103
lines changed

charts/openstack-hypervisor-operator/templates/manager-rbac.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ rules:
1313
- get
1414
- list
1515
- patch
16-
- update
1716
- watch
1817
- apiGroups:
1918
- ""

config/rbac/role.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ rules:
1212
- get
1313
- list
1414
- patch
15-
- update
1615
- watch
1716
- apiGroups:
1817
- ""

internal/controller/gardener_node_lifecycle_controller.go

Lines changed: 25 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import (
3232
corev1ac "k8s.io/client-go/applyconfigurations/core/v1"
3333
v1 "k8s.io/client-go/applyconfigurations/meta/v1"
3434
policyv1ac "k8s.io/client-go/applyconfigurations/policy/v1"
35-
"k8s.io/client-go/util/retry"
3635
ctrl "sigs.k8s.io/controller-runtime"
3736
k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
3837
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
@@ -51,114 +50,79 @@ const (
5150
labelDeployment = "cobaltcore-maintenance-controller"
5251
maintenancePodsNamespace = "kube-system"
5352
labelCriticalComponent = "node.gardener.cloud/critical-component"
54-
valueReasonTerminating = "terminating"
5553
MaintenanceControllerName = "maintenance"
5654
)
5755

5856
// The counter-side in gardener is here:
5957
// https://github.com/gardener/machine-controller-manager/blob/rel-v0.56/pkg/util/provider/machinecontroller/machine.go#L646
6058

61-
// +kubebuilder:rbac:groups="",resources=nodes,verbs=get;list;watch;patch;update;watch
59+
// +kubebuilder:rbac:groups=kvm.cloud.sap,resources=hypervisors,verbs=get;list;watch;update;patch
6260
// +kubebuilder:rbac:groups="apps",resources=deployments,verbs=create;delete;get;list;patch;update;watch
6361
// +kubebuilder:rbac:groups="policy",resources=poddisruptionbudgets,verbs=create;delete;get;list;patch;update;watch
6462

6563
func (r *GardenerNodeLifecycleController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
6664
log := logger.FromContext(ctx).WithName(req.Name)
6765
ctx = logger.IntoContext(ctx, log)
6866

69-
node := &corev1.Node{}
70-
if err := r.Get(ctx, req.NamespacedName, node); err != nil {
71-
// ignore not found errors, could be deleted
67+
hv := &kvmv1.Hypervisor{}
68+
if err := r.Get(ctx, req.NamespacedName, hv); err != nil {
7269
return ctrl.Result{}, k8sclient.IgnoreNotFound(err)
7370
}
7471

75-
hv := kvmv1.Hypervisor{}
76-
if err := r.Get(ctx, k8sclient.ObjectKey{Name: req.Name}, &hv); k8sclient.IgnoreNotFound(err) != nil {
77-
return ctrl.Result{}, err
78-
}
7972
if !hv.Spec.LifecycleEnabled {
8073
// Nothing to be done
8174
return ctrl.Result{}, nil
8275
}
8376

84-
if isTerminating(node) {
85-
changed, err := setNodeLabels(ctx, r.Client, node, map[string]string{labelEvictionRequired: valueReasonTerminating})
86-
if changed || err != nil {
87-
return ctrl.Result{}, err
88-
}
89-
}
90-
91-
// We do not care about the particular value, as long as it isn't an error
92-
var minAvailable int32 = 1
93-
evictionValue, found := node.Labels[labelEvictionApproved]
94-
if found && evictionValue != "false" {
95-
minAvailable = 0
77+
var minAvailable int32 = 0
78+
if !meta.IsStatusConditionFalse(hv.Status.Conditions, kvmv1.ConditionTypeEvicting) {
79+
// Evicting condition is either not present or is true (i.e. ongoing)
80+
minAvailable = 1 // Do not allow draining of the pod
9681
}
9782

98-
if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
99-
return r.ensureBlockingPodDisruptionBudget(ctx, node, minAvailable)
100-
}); err != nil {
83+
if err := r.ensureBlockingPodDisruptionBudget(ctx, hv, minAvailable); err != nil {
10184
return ctrl.Result{}, err
10285
}
10386

10487
onboardingCompleted := meta.IsStatusConditionFalse(hv.Status.Conditions, ConditionTypeOnboarding)
105-
106-
if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
107-
return r.ensureSignallingDeployment(ctx, node, minAvailable, onboardingCompleted)
108-
}); err != nil {
88+
if err := r.ensureSignallingDeployment(ctx, hv, minAvailable, onboardingCompleted); err != nil {
10989
return ctrl.Result{}, err
11090
}
11191

11292
return ctrl.Result{}, nil
11393
}
11494

115-
func (r *GardenerNodeLifecycleController) ensureBlockingPodDisruptionBudget(ctx context.Context, node *corev1.Node, minAvailable int32) error {
116-
name := nameForNode(node)
117-
nodeLabels := labelsForNode(node)
118-
gvk, err := apiutil.GVKForObject(node, r.Scheme)
95+
func (r *GardenerNodeLifecycleController) ensureBlockingPodDisruptionBudget(ctx context.Context, hypervisor *kvmv1.Hypervisor, minAvailable int32) error {
96+
name := nameForHypervisor(hypervisor)
97+
nodeLabels := labelsForHypervisor(hypervisor)
98+
gvk, err := apiutil.GVKForObject(hypervisor, r.Scheme)
11999
if err != nil {
120100
return err
121101
}
122102

123103
podDisruptionBudget := policyv1ac.PodDisruptionBudget(name, maintenancePodsNamespace).
124104
WithLabels(nodeLabels).
125-
WithOwnerReferences(OwnerReference(node, &gvk)).
105+
WithOwnerReferences(OwnerReference(hypervisor, &gvk)).
126106
WithSpec(policyv1ac.PodDisruptionBudgetSpec().
127107
WithMinAvailable(intstr.FromInt32(minAvailable)).
128108
WithSelector(v1.LabelSelector().WithMatchLabels(nodeLabels)))
129109

130110
return r.Apply(ctx, podDisruptionBudget, k8sclient.FieldOwner(MaintenanceControllerName))
131111
}
132112

133-
func isTerminating(node *corev1.Node) bool {
134-
conditions := node.Status.Conditions
135-
if conditions == nil {
136-
return false
137-
}
138-
139-
// See: https://github.com/gardener/machine-controller-manager/blob/rel-v0.56/pkg/util/provider/machinecontroller/machine.go#L658-L659
140-
for _, condition := range conditions {
141-
if condition.Type == "Terminating" {
142-
return true
143-
}
144-
}
145-
146-
return false
147-
}
148-
149-
func nameForNode(node *corev1.Node) string {
150-
return fmt.Sprintf("maint-%v", node.Name)
113+
func nameForHypervisor(hypervisor *kvmv1.Hypervisor) string {
114+
return fmt.Sprintf("maint-%v", hypervisor.Name)
151115
}
152116

153-
func labelsForNode(node *corev1.Node) map[string]string {
117+
func labelsForHypervisor(hypervisor *kvmv1.Hypervisor) map[string]string {
154118
return map[string]string{
155-
labelDeployment: nameForNode(node),
119+
labelDeployment: nameForHypervisor(hypervisor),
156120
}
157121
}
158122

159-
func (r *GardenerNodeLifecycleController) ensureSignallingDeployment(ctx context.Context, node *corev1.Node, scale int32, ready bool) error {
160-
name := nameForNode(node)
161-
labels := labelsForNode(node)
123+
func (r *GardenerNodeLifecycleController) ensureSignallingDeployment(ctx context.Context, hypervisor *kvmv1.Hypervisor, scale int32, ready bool) error {
124+
name := nameForHypervisor(hypervisor)
125+
labels := labelsForHypervisor(hypervisor)
162126

163127
podLabels := maps.Clone(labels)
164128
podLabels[labelCriticalComponent] = "true"
@@ -170,13 +134,13 @@ func (r *GardenerNodeLifecycleController) ensureSignallingDeployment(ctx context
170134
command = "/bin/false"
171135
}
172136

173-
gvk, err := apiutil.GVKForObject(node, r.Scheme)
137+
gvk, err := apiutil.GVKForObject(hypervisor, r.Scheme)
174138
if err != nil {
175139
return err
176140
}
177141

178142
deployment := apps1ac.Deployment(name, maintenancePodsNamespace).
179-
WithOwnerReferences(OwnerReference(node, &gvk)).
143+
WithOwnerReferences(OwnerReference(hypervisor, &gvk)).
180144
WithLabels(labels).
181145
WithSpec(apps1ac.DeploymentSpec().
182146
WithReplicas(scale).
@@ -191,7 +155,7 @@ func (r *GardenerNodeLifecycleController) ensureSignallingDeployment(ctx context
191155
WithSpec(corev1ac.PodSpec().
192156
WithHostNetwork(true).
193157
WithNodeSelector(map[string]string{
194-
corev1.LabelHostname: node.Labels[corev1.LabelHostname],
158+
corev1.LabelHostname: hypervisor.Labels[corev1.LabelHostname],
195159
}).
196160
WithTerminationGracePeriodSeconds(1).
197161
WithTolerations(
@@ -225,7 +189,7 @@ func (r *GardenerNodeLifecycleController) SetupWithManager(mgr ctrl.Manager, nam
225189

226190
return ctrl.NewControllerManagedBy(mgr).
227191
Named(MaintenanceControllerName).
228-
For(&corev1.Node{}).
192+
For(&kvmv1.Hypervisor{}).
229193
Owns(&appsv1.Deployment{}). // trigger the r.Reconcile whenever an Own-ed deployment is created/updated/deleted
230194
Owns(&policyv1.PodDisruptionBudget{}).
231195
Complete(r)

internal/controller/gardener_node_lifecycle_controller_test.go

Lines changed: 99 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,54 +18,127 @@ limitations under the License.
1818
package controller
1919

2020
import (
21+
"context"
22+
"fmt"
23+
2124
. "github.com/onsi/ginkgo/v2"
2225
. "github.com/onsi/gomega"
23-
corev1 "k8s.io/api/core/v1"
26+
appsv1 "k8s.io/api/apps/v1"
27+
policyv1 "k8s.io/api/policy/v1"
28+
"k8s.io/apimachinery/pkg/api/meta"
2429
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2530
"k8s.io/apimachinery/pkg/types"
2631
ctrl "sigs.k8s.io/controller-runtime"
27-
"sigs.k8s.io/controller-runtime/pkg/client"
32+
33+
kvmv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1"
2834
)
2935

30-
var _ = Describe("Gardener Maintenance Controller", func() {
31-
const nodeName = "node-test"
32-
var controller *GardenerNodeLifecycleController
36+
var _ = Describe("GardenerNodeLifecycleController", func() {
37+
var (
38+
controller *GardenerNodeLifecycleController
39+
hypervisorName = types.NamespacedName{Name: "hv-test"}
40+
podName = types.NamespacedName{Name: fmt.Sprintf("maint-%v", hypervisorName.Name), Namespace: "kube-system"}
41+
)
3342

3443
BeforeEach(func() {
3544
controller = &GardenerNodeLifecycleController{
3645
Client: k8sClient,
3746
Scheme: k8sClient.Scheme(),
3847
}
3948

40-
By("creating the namespace for the reconciler")
41-
ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "monsoon3"}}
42-
Expect(client.IgnoreAlreadyExists(k8sClient.Create(ctx, ns))).To(Succeed())
43-
44-
By("creating the core resource for the Kind Node")
45-
resource := &corev1.Node{
49+
By("Creating a Hypervisor resource with LifecycleEnabled")
50+
hypervisor := &kvmv1.Hypervisor{
4651
ObjectMeta: metav1.ObjectMeta{
47-
Name: nodeName,
48-
Labels: map[string]string{labelEvictionRequired: "true"},
52+
Name: hypervisorName.Name,
53+
Namespace: hypervisorName.Namespace,
54+
},
55+
Spec: kvmv1.HypervisorSpec{
56+
LifecycleEnabled: true,
4957
},
5058
}
51-
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
59+
Expect(k8sClient.Create(ctx, hypervisor)).To(Succeed())
60+
DeferCleanup(func(ctx context.Context) {
61+
By("Deleting the Hypervisor resource")
62+
hypervisor := &kvmv1.Hypervisor{}
63+
Expect(k8sClient.Get(ctx, hypervisorName, hypervisor)).To(Succeed())
64+
Expect(k8sClient.Delete(ctx, hypervisor)).To(Succeed())
65+
})
66+
})
67+
68+
// After the setup in JustBefore, we want to reconcile
69+
JustBeforeEach(func(ctx context.Context) {
70+
req := ctrl.Request{NamespacedName: hypervisorName}
71+
for range 3 {
72+
_, err := controller.Reconcile(ctx, req)
73+
Expect(err).NotTo(HaveOccurred())
74+
}
5275
})
5376

54-
AfterEach(func() {
55-
node := &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: nodeName}}
56-
By("Cleanup the specific node")
57-
Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, node))).To(Succeed())
77+
Context("setting the pod disruption budget", func() {
78+
When("not evicted", func() {
79+
It("should set the pdb to minimum 1", func() {
80+
pdb := &policyv1.PodDisruptionBudget{}
81+
Expect(k8sClient.Get(ctx, podName, pdb)).To(Succeed())
82+
Expect(pdb.Spec.MinAvailable).To(HaveValue(HaveField("IntVal", BeEquivalentTo(1))))
83+
})
84+
})
85+
When("evicted", func() {
86+
BeforeEach(func() {
87+
hv := &kvmv1.Hypervisor{}
88+
Expect(k8sClient.Get(ctx, hypervisorName, hv)).To(Succeed())
89+
meta.SetStatusCondition(&hv.Status.Conditions, metav1.Condition{
90+
Type: kvmv1.ConditionTypeEvicting,
91+
Status: metav1.ConditionFalse,
92+
Reason: "dontcare",
93+
Message: "dontcare",
94+
})
95+
Expect(k8sClient.Status().Update(ctx, hv)).To(Succeed())
96+
})
97+
98+
It("should set the pdb to minimum 0", func() {
99+
pdb := &policyv1.PodDisruptionBudget{}
100+
Expect(k8sClient.Get(ctx, podName, pdb)).To(Succeed())
101+
Expect(pdb.Spec.MinAvailable).To(HaveValue(HaveField("IntVal", BeEquivalentTo(0))))
102+
})
103+
})
58104
})
59105

60-
Context("When reconciling a node", func() {
61-
It("should successfully reconcile the resource", func() {
62-
req := ctrl.Request{
63-
NamespacedName: types.NamespacedName{Name: nodeName},
64-
}
106+
Context("create a signalling deployment", func() {
107+
When("onboarding not completed", func() {
108+
It("should create a failing deployment for the node", func() {
109+
deployment := &appsv1.Deployment{}
110+
Expect(k8sClient.Get(ctx, podName, deployment)).To(Succeed())
111+
Expect(deployment.Spec.Template.Spec.Containers).To(
112+
ContainElement(
113+
HaveField("StartupProbe",
114+
HaveField("ProbeHandler",
115+
HaveField("Exec",
116+
HaveField("Command", ContainElements("/bin/false")))))))
117+
})
118+
})
119+
When("onboarding is completed", func() {
120+
BeforeEach(func() {
121+
hv := &kvmv1.Hypervisor{}
122+
Expect(k8sClient.Get(ctx, hypervisorName, hv)).To(Succeed())
123+
meta.SetStatusCondition(&hv.Status.Conditions, metav1.Condition{
124+
Type: ConditionTypeOnboarding,
125+
Status: metav1.ConditionFalse,
126+
Reason: "dontcare",
127+
Message: "dontcare",
128+
})
129+
Expect(k8sClient.Status().Update(ctx, hv)).To(Succeed())
130+
})
65131

66-
By("Reconciling the created resource")
67-
_, err := controller.Reconcile(ctx, req)
68-
Expect(err).NotTo(HaveOccurred())
132+
It("should create a succeeding deployment for the node", func() {
133+
deployment := &appsv1.Deployment{}
134+
Expect(k8sClient.Get(ctx, podName, deployment)).To(Succeed())
135+
Expect(deployment.Spec.Template.Spec.Containers).To(
136+
ContainElement(
137+
HaveField("StartupProbe",
138+
HaveField("ProbeHandler",
139+
HaveField("Exec",
140+
HaveField("Command", ContainElements("/bin/true")))))))
141+
})
69142
})
70143
})
71144
})

internal/controller/utils.go

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,9 @@ package controller
1919

2020
import (
2121
"bytes"
22-
"context"
2322
"errors"
2423
"fmt"
2524
"io"
26-
"maps"
2725
"net/http"
2826
"os"
2927
"slices"
@@ -32,22 +30,10 @@ import (
3230
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3331
"k8s.io/apimachinery/pkg/runtime/schema"
3432
v1ac "k8s.io/client-go/applyconfigurations/meta/v1"
35-
"sigs.k8s.io/controller-runtime/pkg/client"
3633

3734
kvmv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1"
3835
)
3936

40-
// setNodeLabels sets the labels on the node.
41-
func setNodeLabels(ctx context.Context, writer client.Writer, node *corev1.Node, labels map[string]string) (bool, error) {
42-
newNode := node.DeepCopy()
43-
maps.Copy(newNode.Labels, labels)
44-
if maps.Equal(node.Labels, newNode.Labels) {
45-
return false, nil
46-
}
47-
48-
return true, writer.Patch(ctx, newNode, client.MergeFrom(node))
49-
}
50-
5137
func InstanceHaUrl(region, zone, hostname string) string {
5238
if haURL, found := os.LookupEnv("KVM_HA_SERVICE_URL"); found {
5339
return haURL

0 commit comments

Comments
 (0)