Files
Philip Laine ea9f1cb081 Modernize for Go 1.26 (#232)
This change replaces all uses of pointer utils with the new `new`
function which does the same job.

Signed-off-by: Philip Laine <philip.laine@gmail.com>
2026-05-05 13:53:44 +02:00

363 lines
11 KiB
Go

// SPDX-License-Identifier: BSD-3-Clause
package controller
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
netbird "github.com/netbirdio/netbird/shared/management/client/rest"
"github.com/netbirdio/netbird/shared/management/http/api"
nbv1 "github.com/netbirdio/kubernetes-operator/api/v1"
)
var _ = Describe("NBGroup Controller", func() {
Context("When reconciling a resource", func() {
const resourceName = "test-resource"
ctx := context.Background()
typeNamespacedName := types.NamespacedName{
Name: resourceName,
Namespace: "default",
}
var netbirdClient *netbird.Client
var mux *http.ServeMux
var server *httptest.Server
var nbGroup nbv1.NBGroup
BeforeEach(func() {
mux = &http.ServeMux{}
server = httptest.NewServer(mux)
netbirdClient = netbird.New(server.URL, "ABC")
err := k8sClient.Get(ctx, typeNamespacedName, &nbGroup)
if err == nil {
deleteErr := k8sClient.Delete(ctx, &nbGroup)
Expect(deleteErr).NotTo(HaveOccurred())
}
if err == nil || errors.IsNotFound(err) {
nbGroup = nbv1.NBGroup{
ObjectMeta: metav1.ObjectMeta{
Name: resourceName,
Namespace: typeNamespacedName.Namespace,
Finalizers: []string{"netbird.io/group-cleanup"},
},
Spec: nbv1.NBGroupSpec{
Name: resourceName,
},
}
err = k8sClient.Create(ctx, &nbGroup)
Expect(err).NotTo(HaveOccurred())
}
})
AfterEach(func() {
server.Close()
resource := &nbv1.NBGroup{}
err := k8sClient.Get(ctx, typeNamespacedName, resource)
if errors.IsNotFound(err) {
return
}
Expect(err).NotTo(HaveOccurred())
if len(resource.Finalizers) > 0 {
resource.Finalizers = nil
Expect(k8sClient.Update(ctx, resource)).To(Succeed())
}
if resource.DeletionTimestamp == nil {
By("Cleanup the specific resource instance NBGroup")
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
}
})
When("Group doesn't exist", func() {
It("should create group", func() {
By("Reconciling the created resource")
controllerReconciler := &NBGroupReconciler{
Client: k8sClient,
Netbird: netbirdClient,
}
mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
_, err := w.Write([]byte("[]"))
Expect(err).NotTo(HaveOccurred())
} else {
resp := api.Group{
Id: "Test",
Name: resourceName,
}
bs, err := json.Marshal(resp)
Expect(err).NotTo(HaveOccurred())
_, err = w.Write(bs)
Expect(err).NotTo(HaveOccurred())
}
})
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
err = k8sClient.Get(ctx, typeNamespacedName, &nbGroup)
Expect(err).NotTo(HaveOccurred())
Expect(nbGroup.Status.GroupID).NotTo(BeNil())
Expect(*nbGroup.Status.GroupID).To(Equal("Test"))
Expect(nbGroup.Status.Conditions).To(HaveLen(1))
Expect(nbGroup.Status.Conditions[0].Status).To(BeEquivalentTo(metav1.ConditionTrue))
Expect(nbGroup.Status.Conditions[0].Type).To(Equal(nbv1.NBSetupKeyReady))
})
})
When("Group already exists", func() {
It("should use existing group", func() {
By("Reconciling the created resource")
controllerReconciler := &NBGroupReconciler{
Client: k8sClient,
Netbird: netbirdClient,
}
mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) {
resp := []api.Group{
{
Id: "Test",
Name: resourceName,
},
}
bs, err := json.Marshal(resp)
Expect(err).NotTo(HaveOccurred())
_, err = w.Write(bs)
Expect(err).NotTo(HaveOccurred())
})
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
err = k8sClient.Get(ctx, typeNamespacedName, &nbGroup)
Expect(err).NotTo(HaveOccurred())
Expect(nbGroup.Status.GroupID).NotTo(BeNil())
Expect(*nbGroup.Status.GroupID).To(Equal("Test"))
Expect(nbGroup.Status.Conditions).To(HaveLen(1))
Expect(nbGroup.Status.Conditions[0].Status).To(BeEquivalentTo(metav1.ConditionTrue))
Expect(nbGroup.Status.Conditions[0].Type).To(Equal(nbv1.NBSetupKeyReady))
})
})
When("NBGroup is set for deletion", func() {
deleteGroup := func() {
GinkgoHelper()
By("Adding the group ID in status")
nbGroup.Status.GroupID = new("Test")
err := k8sClient.Status().Update(ctx, &nbGroup)
Expect(err).NotTo(HaveOccurred())
By("Deleting the object")
err = k8sClient.Delete(ctx, &nbGroup)
Expect(err).NotTo(HaveOccurred())
}
When("Group is not linked to any resources", func() {
It("should delete group", func() {
deleteGroup()
By("Reconciling the deleting resource")
controllerReconciler := &NBGroupReconciler{
Client: k8sClient,
Netbird: netbirdClient,
}
method := ""
mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) {
method = r.Method
_, err := w.Write([]byte("{}"))
Expect(err).NotTo(HaveOccurred())
})
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
err = k8sClient.Get(ctx, typeNamespacedName, &nbGroup)
Expect(errors.IsNotFound(err)).To(BeTrue())
Expect(method).To(Equal(http.MethodDelete))
})
})
When("Group is linked to other resources", func() {
It("should return error", func() {
deleteGroup()
By("Reconciling the deleting resource")
controllerReconciler := &NBGroupReconciler{
Client: k8sClient,
Netbird: netbirdClient,
}
method := ""
mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) {
method = r.Method
w.WriteHeader(400)
_, err := w.Write([]byte(`{"message": "group has been linked to Policy: meow"}`))
Expect(err).NotTo(HaveOccurred())
})
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).To(HaveOccurred())
err = k8sClient.Get(ctx, typeNamespacedName, &nbGroup)
Expect(errors.IsNotFound(err)).To(BeFalse())
Expect(method).To(Equal(http.MethodDelete))
})
})
When("Group already exists in another namespace", func() {
It("Should delete NBGroup after linked failure", func() {
deleteGroup()
otherGroup := &nbv1.NBGroup{
ObjectMeta: metav1.ObjectMeta{
Name: nbGroup.Name,
Namespace: "kube-system",
},
Spec: nbv1.NBGroupSpec{
Name: nbGroup.Spec.Name,
},
}
Expect(k8sClient.Create(ctx, otherGroup)).To(Succeed())
otherGroup.Status.GroupID = nbGroup.Status.GroupID
Expect(k8sClient.Status().Update(ctx, otherGroup)).To(Succeed())
By("Reconciling the deleting resource")
controllerReconciler := &NBGroupReconciler{
Client: k8sClient,
Netbird: netbirdClient,
}
method := ""
mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) {
method = r.Method
w.WriteHeader(400)
_, err := w.Write([]byte(`{"message": "group has been linked to Policy: meow"}`))
Expect(err).NotTo(HaveOccurred())
})
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
err = k8sClient.Get(ctx, typeNamespacedName, &nbGroup)
Expect(errors.IsNotFound(err)).To(BeTrue())
Expect(method).To(Equal(http.MethodDelete))
})
})
})
When("Group already exists with different ID", func() {
It("should re-use existing group ID", func() {
controllerReconciler := &NBGroupReconciler{
Client: k8sClient,
Netbird: netbirdClient,
}
mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) {
resp := []api.Group{
{
Id: "Test",
Name: resourceName,
},
}
bs, err := json.Marshal(resp)
Expect(err).NotTo(HaveOccurred())
_, err = w.Write(bs)
Expect(err).NotTo(HaveOccurred())
})
nbGroup.Status.GroupID = new("Toast")
Expect(k8sClient.Status().Update(ctx, &nbGroup)).To(Succeed())
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
err = k8sClient.Get(ctx, typeNamespacedName, &nbGroup)
Expect(err).NotTo(HaveOccurred())
Expect(nbGroup.Status.GroupID).NotTo(BeNil())
Expect(*nbGroup.Status.GroupID).To(Equal("Test"))
Expect(nbGroup.Status.Conditions).To(HaveLen(1))
Expect(nbGroup.Status.Conditions[0].Status).To(BeEquivalentTo(metav1.ConditionTrue))
Expect(nbGroup.Status.Conditions[0].Type).To(Equal(nbv1.NBSetupKeyReady))
})
})
When("Group deleted from NetBird API", func() {
It("Should requeue and create group on next run", func() {
controllerReconciler := &NBGroupReconciler{
Client: k8sClient,
Netbird: netbirdClient,
}
mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
resp := []api.Group{}
bs, err := json.Marshal(resp)
Expect(err).NotTo(HaveOccurred())
_, err = w.Write(bs)
Expect(err).NotTo(HaveOccurred())
}
if r.Method == http.MethodPost {
resp := api.Group{
Id: "Test",
Name: resourceName,
}
bs, err := json.Marshal(resp)
Expect(err).NotTo(HaveOccurred())
_, err = w.Write(bs)
Expect(err).NotTo(HaveOccurred())
}
})
nbGroup.Status.GroupID = new("Toast")
Expect(k8sClient.Status().Update(ctx, &nbGroup)).To(Succeed())
res, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
Expect(res.Requeue).To(BeTrue())
err = k8sClient.Get(ctx, typeNamespacedName, &nbGroup)
Expect(err).NotTo(HaveOccurred())
Expect(nbGroup.Status.GroupID).To(BeNil())
Expect(nbGroup.Status.Conditions).To(HaveLen(1))
Expect(nbGroup.Status.Conditions[0].Status).To(BeEquivalentTo(metav1.ConditionFalse))
_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
err = k8sClient.Get(ctx, typeNamespacedName, &nbGroup)
Expect(err).NotTo(HaveOccurred())
Expect(nbGroup.Status.GroupID).NotTo(BeNil())
Expect(*nbGroup.Status.GroupID).To(Equal("Test"))
Expect(nbGroup.Status.Conditions).To(HaveLen(1))
Expect(nbGroup.Status.Conditions[0].Status).To(BeEquivalentTo(metav1.ConditionTrue))
})
})
})
})