Skip to content

Commit 6db11d4

Browse files
committed
Onboard/Offboard nodes with instance-ha service
1 parent e3376ca commit 6db11d4

7 files changed

Lines changed: 87 additions & 10 deletions

File tree

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/go-openapi/swag v0.23.1
1010
github.com/gophercloud/gophercloud/v2 v2.7.0
1111
github.com/gophercloud/utils/v2 v2.0.0-20250711132455-9770683b100a
12+
github.com/jarcoal/httpmock v1.4.0
1213
github.com/onsi/ginkgo/v2 v2.23.4
1314
github.com/onsi/gomega v1.37.0
1415
k8s.io/api v0.33.3

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3Ar
7474
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
7575
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
7676
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
77+
github.com/jarcoal/httpmock v1.4.0 h1:BvhqnH0JAYbNudL2GMJKgOHe2CtKlzJ/5rWKyp+hc2k=
78+
github.com/jarcoal/httpmock v1.4.0/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
7779
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
7880
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
7981
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -90,6 +92,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
9092
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
9193
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
9294
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
95+
github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
96+
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
9397
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
9498
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
9599
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=

internal/controller/maintenance_controller_test.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ package controller
2020
import (
2121
"context"
2222

23-
"github.com/gophercloud/gophercloud/v2/testhelper"
2423
. "github.com/onsi/ginkgo/v2"
2524
. "github.com/onsi/gomega"
2625
corev1 "k8s.io/api/core/v1"
@@ -79,9 +78,6 @@ var _ = Describe("Maintenance Controller", func() {
7978

8079
It("should successfully reconcile the resource", func() {
8180
By("Reconciling the created resource")
82-
testhelper.SetupHTTP()
83-
defer testhelper.TeardownHTTP()
84-
8581
_, err := reconcileNodeLoop(1)
8682
Expect(err).NotTo(HaveOccurred())
8783
})

internal/controller/node_eviction_label_controller.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ func (r *NodeEvictionLabelReconciler) Reconcile(ctx context.Context, req ctrl.Re
9999
}
100100

101101
if value != "" {
102+
err = disableInstanceHA(node)
103+
if err != nil {
104+
return ctrl.Result{}, err
105+
}
106+
102107
newNode := node.DeepCopy()
103108
if value == "true" { //nolint:goconst
104109
evictAgentsLabels(newNode.Labels)
@@ -128,8 +133,12 @@ func (r *NodeEvictionLabelReconciler) reconcileEviction(ctx context.Context, evi
128133
Reason: fmt.Sprintf("openstack-hypervisor-operator: label %v=%v", labelEvictionRequired, maintenanceValue),
129134
}
130135

136+
if err = enableInstanceHAMissingOkay(node); err != nil {
137+
return "", fmt.Errorf("failed to enable instance ha before eviction due to %w", err)
138+
}
139+
131140
if err = r.Create(ctx, eviction); err != nil {
132-
return "", err
141+
return "", fmt.Errorf("failed to create eviction due to %w", err)
133142
}
134143
}
135144

internal/controller/node_eviction_label_controller_test.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package controller
1919

2020
import (
2121
"context"
22+
"fmt"
2223

2324
. "github.com/onsi/ginkgo/v2"
2425
. "github.com/onsi/gomega"
@@ -29,13 +30,17 @@ import (
2930
"sigs.k8s.io/controller-runtime/pkg/client"
3031

3132
kvmv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1"
33+
"github.com/jarcoal/httpmock"
3234
)
3335

3436
var _ = Describe("Node Eviction Label Controller", func() {
3537
var nodeReconciler *NodeEvictionLabelReconciler
3638

3739
Context("When reconciling a node", func() {
38-
const nodeName = "node-test"
40+
const nodeName = "test-node"
41+
const hostName = "test-hostname"
42+
const region = "region"
43+
const zone = "zone"
3944
req := ctrl.Request{
4045
NamespacedName: types.NamespacedName{Name: nodeName},
4146
}
@@ -53,6 +58,7 @@ var _ = Describe("Node Eviction Label Controller", func() {
5358
}
5459

5560
BeforeEach(func() {
61+
httpmock.Activate()
5662
nodeReconciler = &NodeEvictionLabelReconciler{
5763
Client: k8sClient,
5864
Scheme: k8sClient.Scheme(),
@@ -67,9 +73,11 @@ var _ = Describe("Node Eviction Label Controller", func() {
6773
ObjectMeta: metav1.ObjectMeta{
6874
Name: nodeName,
6975
Labels: map[string]string{
70-
corev1.LabelHostname: "test",
71-
labelEvictionRequired: "true",
72-
labelOnboardingState: "completed"},
76+
corev1.LabelHostname: hostName,
77+
corev1.LabelTopologyRegion: region,
78+
corev1.LabelTopologyZone: zone,
79+
labelEvictionRequired: "true",
80+
labelOnboardingState: "completed"},
7381
},
7482
}
7583
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
@@ -79,16 +87,20 @@ var _ = Describe("Node Eviction Label Controller", func() {
7987
node := &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: nodeName}}
8088
By("Cleanup the specific node")
8189
Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, node))).To(Succeed())
90+
httpmock.DeactivateAndReset()
8291
})
8392

8493
It("should successfully reconcile the resource", func() {
94+
url := InstanceHaUrl(region, zone, hostName)
95+
httpmock.RegisterResponder("POST", url, httpmock.NewStringResponder(200, ``))
96+
8597
By("Reconciling the created resource")
8698
_, err := reconcileNodeLoop(5)
8799
Expect(err).NotTo(HaveOccurred())
88100

89101
// expect node controller to create an eviction for the node
90102
err = k8sClient.Get(ctx, types.NamespacedName{
91-
Name: "maintenance-required-test",
103+
Name: fmt.Sprintf("maintenance-required-%v", hostName),
92104
Namespace: "monsoon3",
93105
}, &kvmv1.Eviction{})
94106
Expect(err).NotTo(HaveOccurred())

internal/controller/onboarding_controller.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ func (r *OnboardingController) initialOnboarding(ctx context.Context, node *core
148148
if !found || serviceId == "" {
149149
return fmt.Errorf("empty service-id for label %v on node", labelServiceID)
150150
}
151+
151152
result := services.Update(ctx, r.computeClient, serviceId, services.UpdateOpts{Status: services.ServiceEnabled})
152153
if result.Err != nil {
153154
return result.Err
@@ -194,6 +195,7 @@ func (r *OnboardingController) smokeTest(ctx context.Context, node *corev1.Node,
194195
}
195196

196197
func (r *OnboardingController) completeOnboarding(ctx context.Context, host string, node *corev1.Node) (ctrl.Result, error) {
198+
log := logger.FromContext(ctx)
197199
aggs, err := aggregatesByName(ctx, r.computeClient)
198200
if err != nil {
199201
return ctrl.Result{}, fmt.Errorf("failed to get aggregates %w", err)
@@ -203,6 +205,13 @@ func (r *OnboardingController) completeOnboarding(ctx context.Context, host stri
203205
if err != nil {
204206
return ctrl.Result{}, fmt.Errorf("failed to remove from test aggregate %w", err)
205207
}
208+
log.Info("removed from test-aggregate", "name", testAggregateName)
209+
210+
err = enableInstanceHA(node)
211+
log.Info("enabled instance-ha")
212+
if err != nil {
213+
return ctrl.Result{}, err
214+
}
206215

207216
_, err = setNodeLabels(ctx, r, node, map[string]string{
208217
labelOnboardingState: onboardingValueCompleted,

internal/controller/utils.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@ limitations under the License.
1818
package controller
1919

2020
import (
21+
"bytes"
2122
"context"
23+
"fmt"
2224
"maps"
25+
"net/http"
26+
"slices"
2327

2428
corev1 "k8s.io/api/core/v1"
2529
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -46,3 +50,45 @@ func setNodeAnnotations(ctx context.Context, writer client.Writer, node *corev1.
4650

4751
return writer.Patch(ctx, newNode, client.MergeFrom(node))
4852
}
53+
54+
func InstanceHaUrl(region, zone, hostname string) string {
55+
return fmt.Sprintf("https://kvm-ha-service-%v.%v.cloud.sap/api/hypervisors/%v", zone, region, hostname)
56+
}
57+
58+
func updateInstanceHA(node *corev1.Node, data string, acceptedCodes []int) error {
59+
zone, found := node.Labels[corev1.LabelTopologyZone]
60+
if !found {
61+
return fmt.Errorf("could not find label %v for node", corev1.LabelTopologyZone)
62+
}
63+
region, found := node.Labels[corev1.LabelTopologyRegion]
64+
if !found {
65+
return fmt.Errorf("could not find label %v for node", corev1.LabelTopologyRegion)
66+
}
67+
68+
hostname, found := node.Labels[corev1.LabelHostname]
69+
if !found {
70+
return fmt.Errorf("could not find label %v for node", corev1.LabelHostname)
71+
}
72+
73+
url := InstanceHaUrl(region, zone, hostname)
74+
resp, err := http.Post(url, "application/json", bytes.NewBuffer([]byte(data)))
75+
if err != nil {
76+
return fmt.Errorf("failed to send request to ha service due to %w", err)
77+
}
78+
if !slices.Contains(acceptedCodes, resp.StatusCode) {
79+
return fmt.Errorf("ha service answered with unexpected response %v for %v from %v", resp.StatusCode, data, url)
80+
}
81+
return nil
82+
}
83+
84+
func enableInstanceHA(node *corev1.Node) error {
85+
return updateInstanceHA(node, `{"enabled": true}`, []int{http.StatusOK})
86+
}
87+
88+
func enableInstanceHAMissingOkay(node *corev1.Node) error {
89+
return updateInstanceHA(node, `{"enabled": true}`, []int{http.StatusOK, http.StatusNotFound})
90+
}
91+
92+
func disableInstanceHA(node *corev1.Node) error {
93+
return updateInstanceHA(node, `{"enabled": false}`, []int{http.StatusOK, http.StatusNotFound})
94+
}

0 commit comments

Comments
 (0)