Files
netbird-kubernetes-operator/internal/controller/nbgroup_controller_test.go
T

362 lines
11 KiB
Go
Raw Normal View History

2025-03-06 10:57:45 +02:00
package controller
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
2025-03-06 10:57:45 +02:00
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/api/errors"
2026-04-30 13:39:11 +02:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2025-03-06 10:57:45 +02:00
"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"
2026-04-30 13:39:11 +02:00
nbv1 "github.com/netbirdio/kubernetes-operator/api/v1"
"github.com/netbirdio/kubernetes-operator/internal/util"
2025-03-06 10:57:45 +02:00
)
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",
2025-03-06 10:57:45 +02:00
}
var netbirdClient *netbird.Client
var mux *http.ServeMux
var server *httptest.Server
2026-04-30 13:39:11 +02:00
var nbGroup nbv1.NBGroup
2025-03-06 10:57:45 +02:00
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) {
2026-04-30 13:39:11 +02:00
nbGroup = nbv1.NBGroup{
ObjectMeta: metav1.ObjectMeta{
Name: resourceName,
Namespace: typeNamespacedName.Namespace,
Finalizers: []string{"netbird.io/group-cleanup"},
},
2026-04-30 13:39:11 +02:00
Spec: nbv1.NBGroupSpec{
Name: resourceName,
2025-03-06 10:57:45 +02:00
},
}
err = k8sClient.Create(ctx, &nbGroup)
Expect(err).NotTo(HaveOccurred())
2025-03-06 10:57:45 +02:00
}
})
AfterEach(func() {
server.Close()
2026-04-30 13:39:11 +02:00
resource := &nbv1.NBGroup{}
2025-03-06 10:57:45 +02:00
err := k8sClient.Get(ctx, typeNamespacedName, resource)
if errors.IsNotFound(err) {
return
}
2025-03-06 10:57:45 +02:00
Expect(err).NotTo(HaveOccurred())
if len(resource.Finalizers) > 0 {
resource.Finalizers = nil
Expect(k8sClient.Update(ctx, resource)).To(Succeed())
2025-03-06 10:57:45 +02:00
}
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))
2026-04-30 13:39:11 +02:00
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))
2026-04-30 13:39:11 +02:00
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 = util.Ptr("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()
2026-04-30 13:39:11 +02:00
otherGroup := &nbv1.NBGroup{
ObjectMeta: metav1.ObjectMeta{
Name: nbGroup.Name,
Namespace: "kube-system",
},
2026-04-30 13:39:11 +02:00
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 = util.Ptr("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))
2026-04-30 13:39:11 +02:00
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 = util.Ptr("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))
2026-04-30 13:39:11 +02:00
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))
2026-04-30 13:39:11 +02:00
Expect(nbGroup.Status.Conditions[0].Status).To(BeEquivalentTo(metav1.ConditionTrue))
2025-03-06 10:57:45 +02:00
})
})
})
})