Skip to content

Commit 622ad95

Browse files
committed
Offboarding: Unit test the happy path
1 parent 184e075 commit 622ad95

File tree

1 file changed

+185
-27
lines changed

1 file changed

+185
-27
lines changed

internal/controller/decomission_controller_test.go

Lines changed: 185 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,40 +18,82 @@ limitations under the License.
1818
package controller
1919

2020
import (
21-
"context"
21+
"fmt"
22+
"net/http"
23+
"os"
2224

25+
"github.com/gophercloud/gophercloud/v2/testhelper"
26+
"github.com/gophercloud/gophercloud/v2/testhelper/client"
2327
. "github.com/onsi/ginkgo/v2"
2428
. "github.com/onsi/gomega"
2529
corev1 "k8s.io/api/core/v1"
30+
"k8s.io/apimachinery/pkg/api/meta"
2631
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2732
"k8s.io/apimachinery/pkg/types"
2833
ctrl "sigs.k8s.io/controller-runtime"
29-
"sigs.k8s.io/controller-runtime/pkg/client"
34+
k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
3035

3136
kvmv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1"
3237
)
3338

39+
const (
40+
EOF = "EOF"
41+
serviceId = "service-1234"
42+
hypervisorName = "node-test"
43+
namespaceName = "namespace-test"
44+
AggregateListWithHv = `
45+
{
46+
"aggregates": [
47+
{
48+
"name": "test-aggregate2",
49+
"availability_zone": "",
50+
"deleted": false,
51+
"id": 100001,
52+
"hosts": ["note-test"]
53+
}
54+
]
55+
}
56+
`
57+
AggregateRemoveHostBody = `
58+
{
59+
"aggregate": {
60+
"name": "test-aggregate2",
61+
"availability_zone": "",
62+
"deleted": false,
63+
"id": 100001
64+
}
65+
}`
66+
)
67+
3468
var _ = Describe("Decommission Controller", func() {
35-
const (
36-
namespaceName = "namespace-test"
37-
)
3869
var (
3970
r *NodeDecommissionReconciler
40-
nodeName = types.NamespacedName{Name: "node-test"}
71+
nodeName = types.NamespacedName{Name: hypervisorName}
4172
reconcileReq = ctrl.Request{
4273
NamespacedName: nodeName,
4374
}
75+
fakeServer testhelper.FakeServer
4476
)
4577

4678
BeforeEach(func(ctx SpecContext) {
79+
fakeServer = testhelper.SetupHTTP()
80+
os.Setenv("KVM_HA_SERVICE_URL", fakeServer.Endpoint()+"instance-ha")
81+
82+
DeferCleanup(func() {
83+
os.Unsetenv("KVM_HA_SERVICE_URL")
84+
fakeServer.Teardown()
85+
})
86+
4787
r = &NodeDecommissionReconciler{
48-
Client: k8sClient,
49-
Scheme: k8sClient.Scheme(),
88+
Client: k8sClient,
89+
Scheme: k8sClient.Scheme(),
90+
computeClient: client.ServiceClient(fakeServer),
91+
placementClient: client.ServiceClient(fakeServer),
5092
}
5193

5294
By("creating the namespace for the reconciler")
5395
ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespaceName}}
54-
Expect(client.IgnoreAlreadyExists(k8sClient.Create(ctx, ns))).To(Succeed())
96+
Expect(k8sclient.IgnoreAlreadyExists(k8sClient.Create(ctx, ns))).To(Succeed())
5597

5698
DeferCleanup(func(ctx SpecContext) {
5799
Expect(k8sClient.Delete(ctx, ns)).To(Succeed())
@@ -66,7 +108,15 @@ var _ = Describe("Decommission Controller", func() {
66108
}
67109
Expect(k8sClient.Create(ctx, node)).To(Succeed())
68110
DeferCleanup(func(ctx SpecContext) {
69-
Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, node))).To(Succeed())
111+
node := &corev1.Node{}
112+
Expect(k8sclient.IgnoreNotFound(k8sClient.Get(ctx, nodeName, node))).To(Succeed())
113+
if len(node.Finalizers) > 0 {
114+
node.Finalizers = make([]string, 0)
115+
Expect(k8sClient.Update(ctx, node)).To(Succeed())
116+
}
117+
if node.Name != "" {
118+
Expect(k8sclient.IgnoreNotFound(k8sClient.Delete(ctx, node))).To(Succeed())
119+
}
70120
})
71121

72122
By("Create the hypervisor resource with lifecycle enabled")
@@ -84,32 +134,140 @@ var _ = Describe("Decommission Controller", func() {
84134
})
85135
})
86136

87-
AfterEach(func(ctx context.Context) {
88-
node := &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: nodeName.Name}}
89-
By("Cleanup the specific node and hypervisor resource")
90-
Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, node))).To(Succeed())
91-
92-
// Due to the decommissioning finalizer, we need to reconcile once more to delete the node completely
93-
req := ctrl.Request{
94-
NamespacedName: types.NamespacedName{Name: nodeName.Name},
95-
}
96-
_, err := r.Reconcile(ctx, req)
97-
Expect(err).NotTo(HaveOccurred())
137+
Context("When reconciling a node", func() {
138+
It("should set the finalizer", func(ctx SpecContext) {
139+
By("reconciling the created resource")
140+
_, err := r.Reconcile(ctx, reconcileReq)
141+
Expect(err).NotTo(HaveOccurred())
142+
node := &corev1.Node{}
98143

99-
nodelist := &corev1.NodeList{}
100-
Expect(k8sClient.List(ctx, nodelist)).To(Succeed())
101-
Expect(nodelist.Items).To(BeEmpty())
144+
Expect(k8sClient.Get(ctx, nodeName, node)).To(Succeed())
145+
Expect(node.Finalizers).To(ContainElement(decommissionFinalizerName))
146+
})
102147
})
103148

104-
Context("When reconciling a node", func() {
105-
It("should set the finalizer", func(ctx context.Context) {
106-
By("reconciling the created resource")
149+
Context("When terminating a node", func() {
150+
JustBeforeEach(func(ctx SpecContext) {
151+
By("reconciling first reconciling the to add the finalizer")
107152
_, err := r.Reconcile(ctx, reconcileReq)
108153
Expect(err).NotTo(HaveOccurred())
109154
node := &corev1.Node{}
110155

111156
Expect(k8sClient.Get(ctx, nodeName, node)).To(Succeed())
112157
Expect(node.Finalizers).To(ContainElement(decommissionFinalizerName))
158+
159+
By("and then terminating then node")
160+
node.Status.Conditions = append(node.Status.Conditions, corev1.NodeCondition{
161+
Type: "Terminating",
162+
Status: corev1.ConditionTrue,
163+
Reason: "dontcare",
164+
Message: "dontcare",
165+
})
166+
Expect(k8sClient.Status().Update(ctx, node)).To(Succeed())
167+
Expect(k8sClient.Delete(ctx, node)).To(Succeed())
168+
nodelist := &corev1.NodeList{}
169+
Expect(k8sClient.List(ctx, nodelist)).To(Succeed())
170+
Expect(nodelist.Items).NotTo(BeEmpty())
113171
})
172+
173+
When("the hypervisor was set to ready", func() {
174+
getHypervisorsCalled := 0
175+
BeforeEach(func(ctx SpecContext) {
176+
hv := &kvmv1.Hypervisor{}
177+
Expect(k8sClient.Get(ctx, nodeName, hv)).To(Succeed())
178+
meta.SetStatusCondition(&hv.Status.Conditions,
179+
metav1.Condition{
180+
Type: kvmv1.ConditionTypeReady,
181+
Status: metav1.ConditionTrue,
182+
Reason: "dontcare",
183+
Message: "dontcare",
184+
},
185+
)
186+
Expect(k8sClient.Status().Update(ctx, hv)).To(Succeed())
187+
188+
fakeServer.Mux.HandleFunc("GET /os-hypervisors/detail", func(w http.ResponseWriter, r *http.Request) {
189+
w.Header().Add("Content-Type", "application/json")
190+
w.WriteHeader(http.StatusOK)
191+
getHypervisorsCalled++
192+
Expect(fmt.Fprintf(w, HypervisorWithServers, serviceId, "some reason", hypervisorName)).ToNot(BeNil())
193+
})
194+
195+
fakeServer.Mux.HandleFunc("GET /os-aggregates", func(w http.ResponseWriter, r *http.Request) {
196+
w.Header().Add("Content-Type", "application/json")
197+
w.WriteHeader(http.StatusOK)
198+
199+
_, err := fmt.Fprint(w, AggregateListWithHv)
200+
Expect(err).NotTo(HaveOccurred())
201+
})
202+
203+
fakeServer.Mux.HandleFunc("POST /os-aggregates/100001/action", func(w http.ResponseWriter, r *http.Request) {
204+
// parse request
205+
Expect(r.Header.Get("Content-Type")).To(Equal("application/json"))
206+
expectedBody := `{"remove_host":{"host":"hv-test"}}`
207+
body := make([]byte, r.ContentLength)
208+
_, err := r.Body.Read(body)
209+
Expect(err == nil || err.Error() == EOF).To(BeTrue())
210+
Expect(string(body)).To(MatchJSON(expectedBody))
211+
212+
// send response
213+
w.Header().Add("Content-Type", "application/json")
214+
w.WriteHeader(http.StatusOK)
215+
216+
_, err = fmt.Fprint(w, AggregateRemoveHostBody)
217+
Expect(err).NotTo(HaveOccurred())
218+
})
219+
220+
// c48f6247-abe4-4a24-824e-ea39e108874f comes from the HypervisorWithServers const
221+
fakeServer.Mux.HandleFunc("GET /resource_providers/c48f6247-abe4-4a24-824e-ea39e108874f", func(w http.ResponseWriter, r *http.Request) {
222+
w.Header().Add("Content-Type", "application/json")
223+
w.WriteHeader(http.StatusOK)
224+
225+
_, err := fmt.Fprint(w, `{"uuid": "rp-uuid", "name": "hv-test"}`)
226+
Expect(err).NotTo(HaveOccurred())
227+
})
228+
229+
fakeServer.Mux.HandleFunc("GET /resource_providers/rp-uuid/allocations", func(w http.ResponseWriter, r *http.Request) {
230+
w.Header().Add("Content-Type", "application/json")
231+
w.WriteHeader(http.StatusOK)
232+
233+
_, err := fmt.Fprint(w, `{"allocations": {}}}`)
234+
Expect(err).NotTo(HaveOccurred())
235+
236+
})
237+
fakeServer.Mux.HandleFunc("DELETE /resource_providers/rp-uuid", func(w http.ResponseWriter, r *http.Request) {
238+
w.WriteHeader(http.StatusAccepted)
239+
})
240+
})
241+
242+
It("should set the hypervisor condition", func(ctx SpecContext) {
243+
By("reconciling the created resource")
244+
_, err := r.Reconcile(ctx, reconcileReq)
245+
Expect(err).NotTo(HaveOccurred())
246+
hypervisor := &kvmv1.Hypervisor{}
247+
Expect(k8sClient.Get(ctx, nodeName, hypervisor)).To(Succeed())
248+
Expect(hypervisor.Status.Conditions).To(ContainElement(
249+
SatisfyAll(
250+
HaveField("Type", kvmv1.ConditionTypeReady),
251+
HaveField("Status", metav1.ConditionFalse),
252+
HaveField("Reason", "Decommissioning"),
253+
),
254+
))
255+
})
256+
257+
It("should remove the finalizer", func(ctx SpecContext) {
258+
By("reconciling the created resource")
259+
for range 3 {
260+
_, err := r.Reconcile(ctx, reconcileReq)
261+
Expect(err).NotTo(HaveOccurred())
262+
}
263+
Expect(getHypervisorsCalled).To(BeNumerically(">", 0))
264+
265+
node := &corev1.Node{}
266+
err := k8sClient.Get(ctx, nodeName, node)
267+
Expect(err).To(HaveOccurred())
268+
Expect(k8sclient.IgnoreNotFound(err)).To(Succeed())
269+
})
270+
})
271+
114272
})
115273
})

0 commit comments

Comments
 (0)