Add unit tests to new controllers and fix minor bugs (#12)

This commit is contained in:
M. Essam
2025-03-28 08:55:41 +01:00
committed by GitHub
parent ac8348cda6
commit 6a33bffb65
22 changed files with 3788 additions and 313 deletions
+2 -3
View File
@@ -1,8 +1,7 @@
package v1 package v1
import ( import (
"slices" "github.com/netbirdio/kubernetes-operator/internal/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
@@ -23,7 +22,7 @@ type NBGroupStatus struct {
// Equal returns if NBGroupStatus is equal to this one // Equal returns if NBGroupStatus is equal to this one
func (a NBGroupStatus) Equal(b NBGroupStatus) bool { func (a NBGroupStatus) Equal(b NBGroupStatus) bool {
return a.GroupID == b.GroupID && slices.Equal(a.Conditions, b.Conditions) return (a.GroupID == b.GroupID || (a.GroupID != nil && b.GroupID != nil && *a.GroupID == *b.GroupID)) && util.Equivalent(a.Conditions, b.Conditions)
} }
// +kubebuilder:object:root=true // +kubebuilder:object:root=true
+3 -4
View File
@@ -1,8 +1,7 @@
package v1 package v1
import ( import (
"slices" "github.com/netbirdio/kubernetes-operator/internal/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
@@ -50,8 +49,8 @@ func (a NBPolicyStatus) Equal(b NBPolicyStatus) bool {
return a.TCPPolicyID == b.TCPPolicyID && return a.TCPPolicyID == b.TCPPolicyID &&
a.UDPPolicyID == b.UDPPolicyID && a.UDPPolicyID == b.UDPPolicyID &&
a.LastUpdatedAt == b.LastUpdatedAt && a.LastUpdatedAt == b.LastUpdatedAt &&
slices.Equal(a.ManagedServiceList, b.ManagedServiceList) && util.Equivalent(a.ManagedServiceList, b.ManagedServiceList) &&
slices.Equal(a.Conditions, b.Conditions) util.Equivalent(a.Conditions, b.Conditions)
} }
// +kubebuilder:object:root=true // +kubebuilder:object:root=true
+16 -6
View File
@@ -1,8 +1,7 @@
package v1 package v1
import ( import (
"slices" "github.com/netbirdio/kubernetes-operator/internal/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
@@ -24,6 +23,17 @@ type NBResourceSpec struct {
UDPPorts []int32 `json:"udpPorts,omitempty"` UDPPorts []int32 `json:"udpPorts,omitempty"`
} }
// Equal returns if NBResource is equal to this one
func (a NBResourceSpec) Equal(b NBResourceSpec) bool {
return a.Name == b.Name &&
a.NetworkID == b.NetworkID &&
a.Address == b.Address &&
util.Equivalent(a.Groups, b.Groups) &&
a.PolicyName == b.PolicyName &&
util.Equivalent(a.TCPPorts, b.TCPPorts) &&
util.Equivalent(a.UDPPorts, b.UDPPorts)
}
// NBResourceStatus defines the observed state of NBResource. // NBResourceStatus defines the observed state of NBResource.
type NBResourceStatus struct { type NBResourceStatus struct {
// +optional // +optional
@@ -44,10 +54,10 @@ type NBResourceStatus struct {
func (a NBResourceStatus) Equal(b NBResourceStatus) bool { func (a NBResourceStatus) Equal(b NBResourceStatus) bool {
return a.NetworkResourceID == b.NetworkResourceID && return a.NetworkResourceID == b.NetworkResourceID &&
a.PolicyName == b.PolicyName && a.PolicyName == b.PolicyName &&
slices.Equal(a.TCPPorts, b.TCPPorts) && util.Equivalent(a.TCPPorts, b.TCPPorts) &&
slices.Equal(a.UDPPorts, b.UDPPorts) && util.Equivalent(a.UDPPorts, b.UDPPorts) &&
slices.Equal(a.Groups, b.Groups) && util.Equivalent(a.Groups, b.Groups) &&
slices.Equal(a.Conditions, b.Conditions) util.Equivalent(a.Conditions, b.Conditions)
} }
// +kubebuilder:object:root=true // +kubebuilder:object:root=true
+2 -3
View File
@@ -1,8 +1,7 @@
package v1 package v1
import ( import (
"slices" "github.com/netbirdio/kubernetes-operator/internal/util"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
@@ -40,7 +39,7 @@ func (a NBRoutingPeerStatus) Equal(b NBRoutingPeerStatus) bool {
return a.NetworkID == b.NetworkID && return a.NetworkID == b.NetworkID &&
a.SetupKeyID == b.SetupKeyID && a.SetupKeyID == b.SetupKeyID &&
a.RouterID == b.RouterID && a.RouterID == b.RouterID &&
slices.Equal(a.Conditions, b.Conditions) util.Equivalent(a.Conditions, b.Conditions)
} }
// +kubebuilder:object:root=true // +kubebuilder:object:root=true
+3 -3
View File
@@ -263,17 +263,17 @@ func main() {
} }
if enableWebhooks { if enableWebhooks {
if err = webhooknetbirdiov1.SetupNBResourceWebhookWithManager(mgr, managementURL, netbirdAPIKey); err != nil { if err = webhooknetbirdiov1.SetupNBResourceWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "NBResource") setupLog.Error(err, "unable to create webhook", "webhook", "NBResource")
os.Exit(1) os.Exit(1)
} }
if err = webhooknetbirdiov1.SetupNBRoutingPeerWebhookWithManager(mgr, managementURL, netbirdAPIKey); err != nil { if err = webhooknetbirdiov1.SetupNBRoutingPeerWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "NBRoutingPeer") setupLog.Error(err, "unable to create webhook", "webhook", "NBRoutingPeer")
os.Exit(1) os.Exit(1)
} }
if err = webhooknetbirdiov1.SetupNBGroupWebhookWithManager(mgr, managementURL, netbirdAPIKey); err != nil { if err = webhooknetbirdiov1.SetupNBGroupWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "NBGroup") setupLog.Error(err, "unable to create webhook", "webhook", "NBGroup")
os.Exit(1) os.Exit(1)
} }
+327 -30
View File
@@ -2,16 +2,21 @@ package controller
import ( import (
"context" "context"
"encoding/json"
"net/http"
"net/http/httptest"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/reconcile"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1" netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1"
"github.com/netbirdio/kubernetes-operator/internal/util"
netbird "github.com/netbirdio/netbird/management/client/rest"
"github.com/netbirdio/netbird/management/server/http/api"
) )
var _ = Describe("NBGroup Controller", func() { var _ = Describe("NBGroup Controller", func() {
@@ -22,49 +27,341 @@ var _ = Describe("NBGroup Controller", func() {
typeNamespacedName := types.NamespacedName{ typeNamespacedName := types.NamespacedName{
Name: resourceName, Name: resourceName,
Namespace: "default", // TODO(user):Modify as needed Namespace: "default",
} }
nbgroup := &netbirdiov1.NBGroup{}
var netbirdClient *netbird.Client
var mux *http.ServeMux
var server *httptest.Server
var nbGroup netbirdiov1.NBGroup
BeforeEach(func() { BeforeEach(func() {
Skip("Not implemented yet") mux = &http.ServeMux{}
By("creating the custom resource for the Kind NBGroup") server = httptest.NewServer(mux)
err := k8sClient.Get(ctx, typeNamespacedName, nbgroup) netbirdClient = netbird.New(server.URL, "ABC")
if err != nil && errors.IsNotFound(err) {
resource := &netbirdiov1.NBGroup{ err := k8sClient.Get(ctx, typeNamespacedName, &nbGroup)
ObjectMeta: metav1.ObjectMeta{ if err == nil {
Name: resourceName, deleteErr := k8sClient.Delete(ctx, &nbGroup)
Namespace: "default", Expect(deleteErr).NotTo(HaveOccurred())
}
if err == nil || errors.IsNotFound(err) {
nbGroup = netbirdiov1.NBGroup{
ObjectMeta: v1.ObjectMeta{
Name: resourceName,
Namespace: typeNamespacedName.Namespace,
Finalizers: []string{"netbird.io/group-cleanup"},
},
Spec: netbirdiov1.NBGroupSpec{
Name: resourceName,
}, },
// TODO(user): Specify other spec details if needed.
} }
Expect(k8sClient.Create(ctx, resource)).To(Succeed()) err = k8sClient.Create(ctx, &nbGroup)
Expect(err).NotTo(HaveOccurred())
} }
}) })
AfterEach(func() { AfterEach(func() {
// TODO(user): Cleanup logic after each test, like removing the resource instance. server.Close()
resource := &netbirdiov1.NBGroup{} resource := &netbirdiov1.NBGroup{}
err := k8sClient.Get(ctx, typeNamespacedName, resource) err := k8sClient.Get(ctx, typeNamespacedName, resource)
if errors.IsNotFound(err) {
return
}
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
if len(resource.Finalizers) > 0 {
By("Cleanup the specific resource instance NBGroup") resource.Finalizers = nil
Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) Expect(k8sClient.Update(ctx, resource)).To(Succeed())
})
It("should successfully reconcile the resource", func() {
Skip("Not implemented yet")
By("Reconciling the created resource")
controllerReconciler := &NBGroupReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
} }
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ if resource.DeletionTimestamp == nil {
NamespacedName: typeNamespacedName, 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,
Scheme: k8sClient.Scheme(),
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(v1.ConditionTrue))
Expect(nbGroup.Status.Conditions[0].Type).To(Equal(netbirdiov1.NBSetupKeyReady))
})
})
When("Group already exists", func() {
It("should use existing group", func() {
By("Reconciling the created resource")
controllerReconciler := &NBGroupReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
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(v1.ConditionTrue))
Expect(nbGroup.Status.Conditions[0].Type).To(Equal(netbirdiov1.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,
Scheme: k8sClient.Scheme(),
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,
Scheme: k8sClient.Scheme(),
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 := &netbirdiov1.NBGroup{
ObjectMeta: v1.ObjectMeta{
Name: nbGroup.Name,
Namespace: "kube-system",
},
Spec: netbirdiov1.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,
Scheme: k8sClient.Scheme(),
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,
Scheme: k8sClient.Scheme(),
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))
Expect(nbGroup.Status.Conditions[0].Status).To(BeEquivalentTo(v1.ConditionTrue))
Expect(nbGroup.Status.Conditions[0].Type).To(Equal(netbirdiov1.NBSetupKeyReady))
})
})
When("Group deleted from NetBird API", func() {
It("Should requeue and create group on next run", func() {
controllerReconciler := &NBGroupReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
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))
Expect(nbGroup.Status.Conditions[0].Status).To(BeEquivalentTo(v1.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(v1.ConditionTrue))
}) })
Expect(err).NotTo(HaveOccurred())
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
// Example: If you expect a certain status condition after reconciliation, verify it here.
}) })
}) })
}) })
+23 -15
View File
@@ -35,6 +35,11 @@ var (
errNetBirdAPI = fmt.Errorf("netbird API error") errNetBirdAPI = fmt.Errorf("netbird API error")
) )
const (
protocolTCP = "tcp"
protocolUDP = "udp"
)
// getResources get all NBResource objects in policy.status.managedServiceList // getResources get all NBResource objects in policy.status.managedServiceList
func (r *NBPolicyReconciler) getResources(ctx context.Context, nbPolicy *netbirdiov1.NBPolicy, logger logr.Logger) ([]netbirdiov1.NBResource, error) { func (r *NBPolicyReconciler) getResources(ctx context.Context, nbPolicy *netbirdiov1.NBPolicy, logger logr.Logger) ([]netbirdiov1.NBResource, error) {
var resourceList []netbirdiov1.NBResource var resourceList []netbirdiov1.NBResource
@@ -63,8 +68,8 @@ func (r *NBPolicyReconciler) getResources(ctx context.Context, nbPolicy *netbird
// returns map[protocol] => ports, destination group IDs // returns map[protocol] => ports, destination group IDs
func (r *NBPolicyReconciler) mapResources(ctx context.Context, nbPolicy *netbirdiov1.NBPolicy, resources []netbirdiov1.NBResource, logger logr.Logger) (map[string][]int32, []string, error) { func (r *NBPolicyReconciler) mapResources(ctx context.Context, nbPolicy *netbirdiov1.NBPolicy, resources []netbirdiov1.NBResource, logger logr.Logger) (map[string][]int32, []string, error) {
portMapping := map[string]map[int32]interface{}{ portMapping := map[string]map[int32]interface{}{
"tcp": make(map[int32]interface{}), protocolTCP: make(map[int32]interface{}),
"udp": make(map[int32]interface{}), protocolUDP: make(map[int32]interface{}),
} }
groups, err := r.groupNamesToIDs(ctx, nbPolicy.Spec.DestinationGroups, logger) groups, err := r.groupNamesToIDs(ctx, nbPolicy.Spec.DestinationGroups, logger)
if err != nil { if err != nil {
@@ -77,16 +82,17 @@ func (r *NBPolicyReconciler) mapResources(ctx context.Context, nbPolicy *netbird
groups = append(groups, resource.Status.Groups...) groups = append(groups, resource.Status.Groups...)
for _, p := range resource.Spec.TCPPorts { for _, p := range resource.Spec.TCPPorts {
portMapping["tcp"][p] = nil portMapping[protocolTCP][p] = nil
} }
for _, p := range resource.Spec.UDPPorts { for _, p := range resource.Spec.UDPPorts {
portMapping["udp"][p] = nil portMapping[protocolUDP][p] = nil
} }
} }
} }
ports := make(map[string][]int32) ports := make(map[string][]int32)
for k, vs := range portMapping { for k, vs := range portMapping {
ports[k] = nil
for v := range vs { for v := range vs {
ports[k] = append(ports[k], v) ports[k] = append(ports[k], v)
} }
@@ -131,7 +137,7 @@ func (r *NBPolicyReconciler) createPolicy(ctx context.Context, nbPolicy *netbird
func (r *NBPolicyReconciler) updatePolicy(ctx context.Context, policyID *string, nbPolicy *netbirdiov1.NBPolicy, protocol string, sourceGroupIDs, destinationGroupIDs, ports []string, logger logr.Logger) (*string, bool, error) { func (r *NBPolicyReconciler) updatePolicy(ctx context.Context, policyID *string, nbPolicy *netbirdiov1.NBPolicy, protocol string, sourceGroupIDs, destinationGroupIDs, ports []string, logger logr.Logger) (*string, bool, error) {
policyName := fmt.Sprintf("%s %s", nbPolicy.Spec.Name, strings.ToUpper(protocol)) policyName := fmt.Sprintf("%s %s", nbPolicy.Spec.Name, strings.ToUpper(protocol))
logger.Info("Updating NetBird Policy", "name", policyName, "description", nbPolicy.Spec.Description, "protocol", protocol, "sources", sourceGroupIDs, "destinations", destinationGroupIDs, "ports", ports, "bidirectional", nbPolicy.Spec.Bidirectional) logger.Info("Updating NetBird Policy", "name", policyName, "description", nbPolicy.Spec.Description, "protocol", protocol, "sources", sourceGroupIDs, "destinations", destinationGroupIDs, "ports", ports, "bidirectional", nbPolicy.Spec.Bidirectional)
policy, err := r.netbird.Policies.Update(ctx, *policyID, api.PutApiPoliciesPolicyIdJSONRequestBody{ _, err := r.netbird.Policies.Update(ctx, *policyID, api.PutApiPoliciesPolicyIdJSONRequestBody{
Enabled: true, Enabled: true,
Name: policyName, Name: policyName,
Description: &nbPolicy.Spec.Description, Description: &nbPolicy.Spec.Description,
@@ -163,11 +169,10 @@ func (r *NBPolicyReconciler) updatePolicy(ctx context.Context, policyID *string,
policyID = nil policyID = nil
requeue = true requeue = true
nbPolicy.Status.Conditions = netbirdiov1.NBConditionFalse("Gone", "Policy deleted from NetBird API") nbPolicy.Status.Conditions = netbirdiov1.NBConditionFalse("Gone", "Policy deleted from NetBird API")
} else if err != nil {
return nil, false, err
} }
if err == nil && (policyID == nil || *policy.Id != *policyID) {
policyID = policy.Id
}
return policyID, requeue, nil return policyID, requeue, nil
} }
@@ -192,6 +197,9 @@ func (r *NBPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r
originalPolicy := nbPolicy.DeepCopy() originalPolicy := nbPolicy.DeepCopy()
defer func() { defer func() {
if originalPolicy.DeletionTimestamp != nil && len(nbPolicy.Finalizers) == 0 {
return
}
if !originalPolicy.Status.Equal(nbPolicy.Status) { if !originalPolicy.Status.Equal(nbPolicy.Status) {
updateErr := r.Client.Status().Update(ctx, &nbPolicy) updateErr := r.Client.Status().Update(ctx, &nbPolicy)
if updateErr != nil { if updateErr != nil {
@@ -207,7 +215,7 @@ func (r *NBPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r
if len(nbPolicy.Finalizers) == 0 { if len(nbPolicy.Finalizers) == 0 {
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }
return ctrl.Result{}, r.handleDelete(ctx, nbPolicy, logger) return ctrl.Result{}, r.handleDelete(ctx, &nbPolicy, logger)
} }
resourceList, err := r.getResources(ctx, &nbPolicy, logger) resourceList, err := r.getResources(ctx, &nbPolicy, logger)
@@ -244,9 +252,9 @@ func (r *NBPolicyReconciler) syncPolicy(ctx context.Context, nbPolicy *netbirdio
for protocol, ports := range portMapping { for protocol, ports := range portMapping {
var policyID *string var policyID *string
switch protocol { switch protocol {
case "tcp": case protocolTCP:
policyID = nbPolicy.Status.TCPPolicyID policyID = nbPolicy.Status.TCPPolicyID
case "udp": case protocolUDP:
policyID = nbPolicy.Status.UDPPolicyID policyID = nbPolicy.Status.UDPPolicyID
default: default:
logger.Error(errKubernetesAPI, "Unknown protocol", "protocol", protocol) logger.Error(errKubernetesAPI, "Unknown protocol", "protocol", protocol)
@@ -306,9 +314,9 @@ func (r *NBPolicyReconciler) syncPolicy(ctx context.Context, nbPolicy *netbirdio
} }
switch protocol { switch protocol {
case "tcp": case protocolTCP:
nbPolicy.Status.TCPPolicyID = policyID nbPolicy.Status.TCPPolicyID = policyID
case "udp": case protocolUDP:
nbPolicy.Status.UDPPolicyID = policyID nbPolicy.Status.UDPPolicyID = policyID
default: default:
logger.Error(errKubernetesAPI, "Unknown protocol", "protocol", protocol) logger.Error(errKubernetesAPI, "Unknown protocol", "protocol", protocol)
@@ -320,7 +328,7 @@ func (r *NBPolicyReconciler) syncPolicy(ctx context.Context, nbPolicy *netbirdio
return requeue, nil return requeue, nil
} }
func (r *NBPolicyReconciler) handleDelete(ctx context.Context, nbPolicy netbirdiov1.NBPolicy, logger logr.Logger) error { func (r *NBPolicyReconciler) handleDelete(ctx context.Context, nbPolicy *netbirdiov1.NBPolicy, logger logr.Logger) error {
if nbPolicy.Status.TCPPolicyID != nil { if nbPolicy.Status.TCPPolicyID != nil {
err := r.netbird.Policies.Delete(ctx, *nbPolicy.Status.TCPPolicyID) err := r.netbird.Policies.Delete(ctx, *nbPolicy.Status.TCPPolicyID)
if err != nil && !strings.Contains("not found", err.Error()) { if err != nil && !strings.Contains("not found", err.Error()) {
@@ -337,7 +345,7 @@ func (r *NBPolicyReconciler) handleDelete(ctx context.Context, nbPolicy netbirdi
} }
if util.Contains(nbPolicy.Finalizers, "netbird.io/cleanup") { if util.Contains(nbPolicy.Finalizers, "netbird.io/cleanup") {
nbPolicy.Finalizers = util.Without(nbPolicy.Finalizers, "netbird.io/cleanup") nbPolicy.Finalizers = util.Without(nbPolicy.Finalizers, "netbird.io/cleanup")
err := r.Client.Update(ctx, &nbPolicy) err := r.Client.Update(ctx, nbPolicy)
if err != nil { if err != nil {
logger.Error(errKubernetesAPI, "Error updating NBPolicy", "err", err) logger.Error(errKubernetesAPI, "Error updating NBPolicy", "err", err)
return err return err
+600 -23
View File
@@ -2,7 +2,12 @@ package controller
import ( import (
"context" "context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"github.com/go-logr/logr"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
@@ -12,59 +17,631 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1" netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1"
"github.com/netbirdio/kubernetes-operator/internal/util"
netbird "github.com/netbirdio/netbird/management/client/rest"
"github.com/netbirdio/netbird/management/server/http/api"
ctrl "sigs.k8s.io/controller-runtime"
) )
var _ = Describe("NBPolicy Controller", func() { var _ = Describe("NBPolicy Controller", func() {
Context("When reconciling a resource", func() { Context("When reconciling a resource", func() {
const resourceName = "test-resource" var resourceName = "test-resource"
ctx := context.Background() ctx := context.Background()
typeNamespacedName := types.NamespacedName{ typeNamespacedName := types.NamespacedName{
Name: resourceName, Name: resourceName,
Namespace: "default", // TODO(user):Modify as needed
} }
nbpolicy := &netbirdiov1.NBPolicy{} nbpolicy := &netbirdiov1.NBPolicy{}
var netbirdClient *netbird.Client
var mux *http.ServeMux
var server *httptest.Server
BeforeEach(func() { BeforeEach(func() {
Skip("Not implemented yet") ctrl.SetLogger(logr.New(GinkgoLogr.GetSink()))
mux = &http.ServeMux{}
server = httptest.NewServer(mux)
netbirdClient = netbird.New(server.URL, "ABC")
By("creating the custom resource for the Kind NBPolicy") By("creating the custom resource for the Kind NBPolicy")
err := k8sClient.Get(ctx, typeNamespacedName, nbpolicy) err := k8sClient.Get(ctx, typeNamespacedName, nbpolicy)
if err != nil && errors.IsNotFound(err) { if err != nil && errors.IsNotFound(err) {
resource := &netbirdiov1.NBPolicy{ resource := &netbirdiov1.NBPolicy{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: resourceName, Name: resourceName,
Namespace: "default", Finalizers: []string{"netbird.io/cleanup"},
},
Spec: netbirdiov1.NBPolicySpec{
Name: "Test",
SourceGroups: []string{"All"},
Bidirectional: true,
}, },
// TODO(user): Specify other spec details if needed.
} }
Expect(k8sClient.Create(ctx, resource)).To(Succeed()) Expect(k8sClient.Create(ctx, resource)).To(Succeed())
nbpolicy = resource
} }
}) })
AfterEach(func() { AfterEach(func() {
// TODO(user): Cleanup logic after each test, like removing the resource instance.
resource := &netbirdiov1.NBPolicy{} resource := &netbirdiov1.NBPolicy{}
err := k8sClient.Get(ctx, typeNamespacedName, resource) err := k8sClient.Get(ctx, typeNamespacedName, resource)
Expect(err).NotTo(HaveOccurred()) if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
By("Cleanup the specific resource instance NBPolicy") if len(resource.Finalizers) > 0 {
Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) resource.Finalizers = nil
}) Expect(k8sClient.Update(ctx, resource)).To(Succeed())
It("should successfully reconcile the resource", func() { }
Skip("Not implemented yet")
By("Reconciling the created resource") By("Cleanup the specific resource instance NBPolicy")
controllerReconciler := &NBPolicyReconciler{ Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
Client: k8sClient,
Scheme: k8sClient.Scheme(),
} }
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ nbresource := &netbirdiov1.NBResource{}
NamespacedName: typeNamespacedName, err = k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "test"}, nbresource)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
By("Cleanup the specific resource instance NBResource")
Expect(k8sClient.Delete(ctx, nbresource)).To(Succeed())
}
})
When("Not enough information to create policy", func() {
It("should not create any policy", func() {
controllerReconciler := &NBPolicyReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
netbird: netbirdClient,
ClusterName: "Kubernetes",
}
mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) {
resp := []api.Group{
{
Id: "meow",
Name: "All",
},
}
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())
})
})
When("Enough information to create TCP policy", func() {
It("should create 1 policy", func() {
controllerReconciler := &NBPolicyReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
netbird: netbirdClient,
ClusterName: "Kubernetes",
}
nbResource := &netbirdiov1.NBResource{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: netbirdiov1.NBResourceSpec{
Name: "meow",
Groups: []string{"test"},
NetworkID: "test",
Address: "test.default.svc.cluster.local",
PolicyName: resourceName,
TCPPorts: []int32{443},
},
}
Expect(k8sClient.Create(ctx, nbResource)).To(Succeed())
nbResource.Status = netbirdiov1.NBResourceStatus{
TCPPorts: []int32{443},
PolicyName: &resourceName,
Groups: []string{"test"},
}
Expect(k8sClient.Status().Update(ctx, nbResource)).To(Succeed())
nbpolicy.Status.ManagedServiceList = append(nbpolicy.Status.ManagedServiceList, "default/test")
Expect(k8sClient.Status().Update(ctx, nbpolicy)).To(Succeed())
mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) {
resp := []api.Group{
{
Id: "meow",
Name: "All",
},
}
bs, err := json.Marshal(resp)
Expect(err).NotTo(HaveOccurred())
_, err = w.Write(bs)
Expect(err).NotTo(HaveOccurred())
})
policyCreated := false
mux.HandleFunc("/api/policies", func(w http.ResponseWriter, r *http.Request) {
defer GinkgoRecover()
if r.Method == http.MethodPost {
var policyReq api.PostApiPoliciesJSONRequestBody
bs, err := io.ReadAll(r.Body)
Expect(err).NotTo(HaveOccurred())
err = json.Unmarshal(bs, &policyReq)
Expect(err).NotTo(HaveOccurred())
Expect(policyReq.Name).To(Equal("Test TCP"))
Expect(policyReq.Description).To(Or(BeNil(), BeEquivalentTo(util.Ptr(""))))
Expect(policyReq.Enabled).To(BeTrue())
Expect(policyReq.SourcePostureChecks).To(BeNil())
Expect(policyReq.Rules).To(HaveLen(1))
Expect(policyReq.Rules[0].Action).To(BeEquivalentTo(api.PolicyRuleActionAccept))
Expect(policyReq.Rules[0].Bidirectional).To(BeTrue())
Expect(policyReq.Rules[0].Description).To(Or(BeNil(), BeEquivalentTo(util.Ptr(""))))
Expect(policyReq.Rules[0].DestinationResource).To(BeNil())
Expect(policyReq.Rules[0].Destinations).NotTo(BeNil())
Expect(*policyReq.Rules[0].Destinations).To(HaveLen(1))
Expect((*policyReq.Rules[0].Destinations)[0]).To(Equal("test"))
Expect(policyReq.Rules[0].Enabled).To(BeTrue())
Expect(policyReq.Rules[0].Name).To(Equal("Test TCP"))
Expect(policyReq.Rules[0].Ports).NotTo(BeNil())
Expect((*policyReq.Rules[0].Ports)).To(HaveLen(1))
Expect((*policyReq.Rules[0].Ports)[0]).To(Equal("443"))
Expect(policyReq.Rules[0].Protocol).To(BeEquivalentTo(api.PolicyRuleProtocolTcp))
Expect(policyReq.Rules[0].SourceResource).To(BeNil())
Expect(policyReq.Rules[0].Sources).NotTo(BeNil())
Expect(*policyReq.Rules[0].Sources).To(HaveLen(1))
Expect((*policyReq.Rules[0].Sources)[0]).To(Equal("meow"))
policyCreated = true
resp := api.Policy{
Id: &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())
Expect(policyCreated).To(BeTrue())
})
})
When("TCP information no longer sufficient", func() {
It("should delete tcp policy", func() {
controllerReconciler := &NBPolicyReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
netbird: netbirdClient,
ClusterName: "Kubernetes",
}
nbpolicy.Status.ManagedServiceList = append(nbpolicy.Status.ManagedServiceList, "default/noexist")
nbpolicy.Status.TCPPolicyID = util.Ptr("policyid")
Expect(k8sClient.Status().Update(ctx, nbpolicy)).To(Succeed())
mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) {
resp := []api.Group{
{
Id: "meow",
Name: "All",
},
}
bs, err := json.Marshal(resp)
Expect(err).NotTo(HaveOccurred())
_, err = w.Write(bs)
Expect(err).NotTo(HaveOccurred())
})
policyDeleted := false
mux.HandleFunc("/api/policies/policyid", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodDelete {
policyDeleted = true
_, err := w.Write([]byte("{}"))
Expect(err).NotTo(HaveOccurred())
}
})
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
Expect(policyDeleted).To(BeTrue())
})
})
When("Enough information to create UDP policy", func() {
It("should create 1 policy", func() {
controllerReconciler := &NBPolicyReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
netbird: netbirdClient,
ClusterName: "Kubernetes",
}
nbResource := &netbirdiov1.NBResource{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: netbirdiov1.NBResourceSpec{
Name: "meow",
Groups: []string{"test"},
NetworkID: "test",
Address: "test.default.svc.cluster.local",
PolicyName: resourceName,
UDPPorts: []int32{443},
},
}
Expect(k8sClient.Create(ctx, nbResource)).To(Succeed())
nbResource.Status = netbirdiov1.NBResourceStatus{
UDPPorts: []int32{443},
PolicyName: &resourceName,
Groups: []string{"test"},
}
Expect(k8sClient.Status().Update(ctx, nbResource)).To(Succeed())
nbpolicy.Status.ManagedServiceList = append(nbpolicy.Status.ManagedServiceList, "default/test")
Expect(k8sClient.Status().Update(ctx, nbpolicy)).To(Succeed())
mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) {
resp := []api.Group{
{
Id: "meow",
Name: "All",
},
}
bs, err := json.Marshal(resp)
Expect(err).NotTo(HaveOccurred())
_, err = w.Write(bs)
Expect(err).NotTo(HaveOccurred())
})
policyCreated := false
mux.HandleFunc("/api/policies", func(w http.ResponseWriter, r *http.Request) {
defer GinkgoRecover()
if r.Method == http.MethodPost {
var policyReq api.PostApiPoliciesJSONRequestBody
bs, err := io.ReadAll(r.Body)
Expect(err).NotTo(HaveOccurred())
err = json.Unmarshal(bs, &policyReq)
Expect(err).NotTo(HaveOccurred())
Expect(policyReq.Name).To(Equal("Test UDP"))
Expect(policyReq.Description).To(Or(BeNil(), BeEquivalentTo(util.Ptr(""))))
Expect(policyReq.Enabled).To(BeTrue())
Expect(policyReq.SourcePostureChecks).To(BeNil())
Expect(policyReq.Rules).To(HaveLen(1))
Expect(policyReq.Rules[0].Action).To(BeEquivalentTo(api.PolicyRuleActionAccept))
Expect(policyReq.Rules[0].Bidirectional).To(BeTrue())
Expect(policyReq.Rules[0].Description).To(Or(BeNil(), BeEquivalentTo(util.Ptr(""))))
Expect(policyReq.Rules[0].DestinationResource).To(BeNil())
Expect(policyReq.Rules[0].Destinations).NotTo(BeNil())
Expect(*policyReq.Rules[0].Destinations).To(HaveLen(1))
Expect((*policyReq.Rules[0].Destinations)[0]).To(Equal("test"))
Expect(policyReq.Rules[0].Enabled).To(BeTrue())
Expect(policyReq.Rules[0].Name).To(Equal("Test UDP"))
Expect(policyReq.Rules[0].Ports).NotTo(BeNil())
Expect((*policyReq.Rules[0].Ports)).To(HaveLen(1))
Expect((*policyReq.Rules[0].Ports)[0]).To(Equal("443"))
Expect(policyReq.Rules[0].Protocol).To(BeEquivalentTo(api.PolicyRuleProtocolUdp))
Expect(policyReq.Rules[0].SourceResource).To(BeNil())
Expect(policyReq.Rules[0].Sources).NotTo(BeNil())
Expect(*policyReq.Rules[0].Sources).To(HaveLen(1))
Expect((*policyReq.Rules[0].Sources)[0]).To(Equal("meow"))
policyCreated = true
resp := api.Policy{
Id: &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())
Expect(policyCreated).To(BeTrue())
})
})
When("UDP information no longer sufficient", func() {
It("should delete udp policy", func() {
controllerReconciler := &NBPolicyReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
netbird: netbirdClient,
ClusterName: "Kubernetes",
}
nbpolicy.Status.ManagedServiceList = append(nbpolicy.Status.ManagedServiceList, "default/noexist")
nbpolicy.Status.UDPPolicyID = util.Ptr("policyid")
Expect(k8sClient.Status().Update(ctx, nbpolicy)).To(Succeed())
mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) {
resp := []api.Group{
{
Id: "meow",
Name: "All",
},
}
bs, err := json.Marshal(resp)
Expect(err).NotTo(HaveOccurred())
_, err = w.Write(bs)
Expect(err).NotTo(HaveOccurred())
})
policyDeleted := false
mux.HandleFunc("/api/policies/policyid", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodDelete {
policyDeleted = true
_, err := w.Write([]byte("{}"))
Expect(err).NotTo(HaveOccurred())
}
})
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
Expect(policyDeleted).To(BeTrue())
})
})
When("Existing protocol gets restricted", func() {
It("Should delete protocol policy", func() {
controllerReconciler := &NBPolicyReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
netbird: netbirdClient,
ClusterName: "Kubernetes",
}
nbResource := &netbirdiov1.NBResource{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: netbirdiov1.NBResourceSpec{
Name: "meow",
Groups: []string{"test"},
NetworkID: "test",
Address: "test.default.svc.cluster.local",
PolicyName: resourceName,
TCPPorts: []int32{443},
},
}
Expect(k8sClient.Create(ctx, nbResource)).To(Succeed())
nbResource.Status = netbirdiov1.NBResourceStatus{
TCPPorts: []int32{443},
PolicyName: &resourceName,
Groups: []string{"test"},
}
Expect(k8sClient.Status().Update(ctx, nbResource)).To(Succeed())
nbpolicy.Spec.Protocols = []string{"udp"}
Expect(k8sClient.Update(ctx, nbpolicy)).To(Succeed())
nbpolicy.Status.ManagedServiceList = append(nbpolicy.Status.ManagedServiceList, "default/test")
nbpolicy.Status.TCPPolicyID = util.Ptr("policyid")
Expect(k8sClient.Status().Update(ctx, nbpolicy)).To(Succeed())
mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) {
resp := []api.Group{
{
Id: "meow",
Name: "All",
},
}
bs, err := json.Marshal(resp)
Expect(err).NotTo(HaveOccurred())
_, err = w.Write(bs)
Expect(err).NotTo(HaveOccurred())
})
policyDeleted := false
mux.HandleFunc("/api/policies/policyid", func(w http.ResponseWriter, r *http.Request) {
defer GinkgoRecover()
if r.Method == http.MethodDelete {
policyDeleted = true
_, err := w.Write([]byte("{}"))
Expect(err).NotTo(HaveOccurred())
}
})
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
Expect(policyDeleted).To(BeTrue())
})
})
When("Updating existing policy", func() {
AfterEach(func() {
nbresource := &netbirdiov1.NBResource{}
err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "test-b"}, nbresource)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
By("Cleanup the specific resource instance NBResource")
Expect(k8sClient.Delete(ctx, nbresource)).To(Succeed())
}
})
It("Should give all information to Update method", func() {
controllerReconciler := &NBPolicyReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
netbird: netbirdClient,
ClusterName: "Kubernetes",
}
nbResource := &netbirdiov1.NBResource{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: netbirdiov1.NBResourceSpec{
Name: "meow",
Groups: []string{"test"},
NetworkID: "test",
Address: "test.default.svc.cluster.local",
PolicyName: resourceName,
TCPPorts: []int32{443},
},
}
Expect(k8sClient.Create(ctx, nbResource)).To(Succeed())
nbResource.Status = netbirdiov1.NBResourceStatus{
TCPPorts: []int32{443},
PolicyName: &resourceName,
Groups: []string{"test"},
}
Expect(k8sClient.Status().Update(ctx, nbResource)).To(Succeed())
nbResourceB := &netbirdiov1.NBResource{
ObjectMeta: metav1.ObjectMeta{
Name: "test-b",
Namespace: "default",
},
Spec: netbirdiov1.NBResourceSpec{
Name: "meow-b",
Groups: []string{"test-b"},
NetworkID: "test",
Address: "test-b.default.svc.cluster.local",
PolicyName: resourceName,
TCPPorts: []int32{80},
},
}
Expect(k8sClient.Create(ctx, nbResourceB)).To(Succeed())
nbResourceB.Status = netbirdiov1.NBResourceStatus{
TCPPorts: []int32{80},
PolicyName: &resourceName,
Groups: []string{"test-b"},
}
Expect(k8sClient.Status().Update(ctx, nbResourceB)).To(Succeed())
nbpolicy.Status.ManagedServiceList = append(nbpolicy.Status.ManagedServiceList, "default/test", "default/test-b")
nbpolicy.Status.TCPPolicyID = util.Ptr("policyid")
Expect(k8sClient.Status().Update(ctx, nbpolicy)).To(Succeed())
mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) {
resp := []api.Group{
{
Id: "meow",
Name: "All",
},
}
bs, err := json.Marshal(resp)
Expect(err).NotTo(HaveOccurred())
_, err = w.Write(bs)
Expect(err).NotTo(HaveOccurred())
})
policyUpdated := false
mux.HandleFunc("/api/policies/policyid", func(w http.ResponseWriter, r *http.Request) {
defer GinkgoRecover()
if r.Method == http.MethodPut {
policyUpdated = true
var policyReq api.PostApiPoliciesJSONRequestBody
bs, err := io.ReadAll(r.Body)
Expect(err).NotTo(HaveOccurred())
err = json.Unmarshal(bs, &policyReq)
Expect(err).NotTo(HaveOccurred())
Expect(policyReq.Name).To(Equal("Test TCP"))
Expect(policyReq.Description).To(Or(BeNil(), BeEquivalentTo(util.Ptr(""))))
Expect(policyReq.Enabled).To(BeTrue())
Expect(policyReq.SourcePostureChecks).To(BeNil())
Expect(policyReq.Rules).To(HaveLen(1))
Expect(policyReq.Rules[0].Action).To(BeEquivalentTo(api.PolicyRuleActionAccept))
Expect(policyReq.Rules[0].Bidirectional).To(BeTrue())
Expect(policyReq.Rules[0].Description).To(Or(BeNil(), BeEquivalentTo(util.Ptr(""))))
Expect(policyReq.Rules[0].DestinationResource).To(BeNil())
Expect(policyReq.Rules[0].Destinations).NotTo(BeNil())
Expect(*policyReq.Rules[0].Destinations).To(HaveLen(2))
Expect((*policyReq.Rules[0].Destinations)).To(ConsistOf([]string{"test", "test-b"}))
Expect(policyReq.Rules[0].Enabled).To(BeTrue())
Expect(policyReq.Rules[0].Name).To(Equal("Test TCP"))
Expect(policyReq.Rules[0].Ports).NotTo(BeNil())
Expect((*policyReq.Rules[0].Ports)).To(HaveLen(2))
Expect((*policyReq.Rules[0].Ports)).To(ConsistOf([]string{"443", "80"}))
Expect(policyReq.Rules[0].Protocol).To(BeEquivalentTo(api.PolicyRuleProtocolTcp))
Expect(policyReq.Rules[0].SourceResource).To(BeNil())
Expect(policyReq.Rules[0].Sources).NotTo(BeNil())
Expect(*policyReq.Rules[0].Sources).To(HaveLen(1))
Expect((*policyReq.Rules[0].Sources)[0]).To(Equal("meow"))
_, err = w.Write([]byte("{}"))
Expect(err).NotTo(HaveOccurred())
}
})
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
Expect(policyUpdated).To(BeTrue())
})
})
When("NBPolicy is set for deletion", func() {
It("should delete Policies", func() {
controllerReconciler := &NBPolicyReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
netbird: netbirdClient,
ClusterName: "Kubernetes",
}
nbpolicy.Status.TCPPolicyID = util.Ptr("policyidtcp")
nbpolicy.Status.UDPPolicyID = util.Ptr("policyidudp")
Expect(k8sClient.Status().Update(ctx, nbpolicy)).To(Succeed())
Expect(k8sClient.Delete(ctx, nbpolicy)).To(Succeed())
tcpPolicyDeleted := false
mux.HandleFunc("/api/policies/policyidtcp", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodDelete {
tcpPolicyDeleted = true
_, err := w.Write([]byte("{}"))
Expect(err).NotTo(HaveOccurred())
}
})
udpPolicyDeleted := false
mux.HandleFunc("/api/policies/policyidudp", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodDelete {
udpPolicyDeleted = true
_, err := w.Write([]byte("{}"))
Expect(err).NotTo(HaveOccurred())
}
})
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
Expect(tcpPolicyDeleted).To(BeTrue())
Expect(udpPolicyDeleted).To(BeTrue())
err = k8sClient.Get(ctx, typeNamespacedName, nbpolicy)
Expect(errors.IsNotFound(err)).To(BeTrue())
}) })
Expect(err).NotTo(HaveOccurred())
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
// Example: If you expect a certain status condition after reconciliation, verify it here.
}) })
}) })
}) })
+83 -10
View File
@@ -3,6 +3,7 @@ package controller
import ( import (
"context" "context"
"fmt" "fmt"
"slices"
"strings" "strings"
"time" "time"
@@ -48,6 +49,9 @@ func (r *NBResourceReconciler) Reconcile(ctx context.Context, req ctrl.Request)
originalResource := nbResource.DeepCopy() originalResource := nbResource.DeepCopy()
defer func() { defer func() {
if originalResource.DeletionTimestamp != nil && len(nbResource.Finalizers) == 0 {
return
}
if !originalResource.Status.Equal(nbResource.Status) { if !originalResource.Status.Equal(nbResource.Status) {
updateErr := r.Client.Status().Update(ctx, nbResource) updateErr := r.Client.Status().Update(ctx, nbResource)
if updateErr != nil { if updateErr != nil {
@@ -110,8 +114,8 @@ func (r *NBResourceReconciler) handlePolicy(ctx context.Context, req ctrl.Reques
var nbPolicy netbirdiov1.NBPolicy var nbPolicy netbirdiov1.NBPolicy
if nbResource.Spec.PolicyName == "" && nbResource.Status.PolicyName != nil { if nbResource.Spec.PolicyName == "" && nbResource.Status.PolicyName != nil {
// Remove self reference from policy status // Remove self reference from policy status
nbResource.Status.PolicyName = nil
err := r.Client.Get(ctx, types.NamespacedName{Name: *nbResource.Status.PolicyName}, &nbPolicy) err := r.Client.Get(ctx, types.NamespacedName{Name: *nbResource.Status.PolicyName}, &nbPolicy)
nbResource.Status.PolicyName = nil
if err != nil { if err != nil {
logger.Error(errKubernetesAPI, "error getting NBPolicy", "err", err, "policyName", nbResource.Spec.PolicyName) logger.Error(errKubernetesAPI, "error getting NBPolicy", "err", err, "policyName", nbResource.Spec.PolicyName)
return err return err
@@ -123,7 +127,24 @@ func (r *NBResourceReconciler) handlePolicy(ctx context.Context, req ctrl.Reques
} }
} else { } else {
// Update policy settings if any difference is found // Update policy settings if any difference is found
// TODO: Handle updated policy name by removing reference from old policy name in status.policyName if nbResource.Status.PolicyName != nil {
err := r.Client.Get(ctx, types.NamespacedName{Name: *nbResource.Status.PolicyName}, &nbPolicy)
if !errors.IsNotFound(err) {
if err != nil {
logger.Error(errKubernetesAPI, "error getting NBPolicy", "err", err, "policyName", nbResource.Spec.PolicyName)
return err
}
if util.Contains(nbPolicy.Status.ManagedServiceList, req.NamespacedName.String()) {
nbPolicy.Status.ManagedServiceList = util.Without(nbPolicy.Status.ManagedServiceList, req.NamespacedName.String())
err := r.Client.Status().Update(ctx, &nbPolicy)
if err != nil {
logger.Error(errKubernetesAPI, "error updating NBPolicy", "err", err, "policyName", nbResource.Spec.PolicyName)
return err
}
}
}
}
err := r.Client.Get(ctx, types.NamespacedName{Name: nbResource.Spec.PolicyName}, &nbPolicy) err := r.Client.Get(ctx, types.NamespacedName{Name: nbResource.Spec.PolicyName}, &nbPolicy)
if err != nil { if err != nil {
logger.Error(errKubernetesAPI, "error getting NBPolicy", "err", err, "policyName", nbResource.Spec.PolicyName) logger.Error(errKubernetesAPI, "error getting NBPolicy", "err", err, "policyName", nbResource.Spec.PolicyName)
@@ -230,8 +251,6 @@ func (r *NBResourceReconciler) handleNetBirdResource(ctx context.Context, nbReso
return nil, err return nil, err
} }
nbResource.Status.NetworkResourceID = &resource.Id
} else if nbResource.Status.NetworkResourceID == nil && resource != nil {
nbResource.Status.NetworkResourceID = &resource.Id nbResource.Status.NetworkResourceID = &resource.Id
} else if resource == nil { } else if resource == nil {
// Status remembers networkResourceID but resource was deleted elsewhere // Status remembers networkResourceID but resource was deleted elsewhere
@@ -265,6 +284,44 @@ func (r *NBResourceReconciler) handleNetBirdResource(ctx context.Context, nbReso
// handleGroups create NBGroup objects for each group specified in NBResource // handleGroups create NBGroup objects for each group specified in NBResource
func (r *NBResourceReconciler) handleGroups(ctx context.Context, req ctrl.Request, nbResource *netbirdiov1.NBResource, logger logr.Logger) ([]string, *ctrl.Result, error) { func (r *NBResourceReconciler) handleGroups(ctx context.Context, req ctrl.Request, nbResource *netbirdiov1.NBResource, logger logr.Logger) ([]string, *ctrl.Result, error) {
nbGroupList := netbirdiov1.NBGroupList{}
err := r.Client.List(ctx, &nbGroupList, &client.ListOptions{Namespace: req.Namespace})
if err != nil {
logger.Error(errKubernetesAPI, "error listing NBGroup", "err", err)
return nil, nil, err
}
for _, g := range nbGroupList.Items {
ownerIndex := -1
for idx, o := range g.OwnerReferences {
if o.UID == nbResource.UID {
ownerIndex = idx
break
}
}
if ownerIndex == -1 {
continue
}
if util.Contains(nbResource.Spec.Groups, g.Spec.Name) {
continue
}
if len(g.OwnerReferences) > 1 {
g.OwnerReferences = slices.Delete(g.OwnerReferences, ownerIndex, ownerIndex+1)
err = r.Client.Update(ctx, &g)
if err != nil && !errors.IsNotFound(err) {
logger.Error(errKubernetesAPI, "error updating NBGroup", "err", err)
return nil, nil, err
}
} else if len(g.OwnerReferences) == 1 {
g.Finalizers = util.Without(g.Finalizers, "netbird.io/resource-cleanup")
err = r.Client.Update(ctx, &g)
if err != nil && !errors.IsNotFound(err) {
logger.Error(errKubernetesAPI, "error updating NBGroup", "err", err)
return nil, nil, err
}
}
}
var groupIDs []string var groupIDs []string
for _, groupName := range nbResource.Spec.Groups { for _, groupName := range nbResource.Spec.Groups {
@@ -283,8 +340,8 @@ func (r *NBResourceReconciler) handleGroups(ctx context.Context, req ctrl.Reques
Namespace: nbResource.Namespace, Namespace: nbResource.Namespace,
OwnerReferences: []v1.OwnerReference{ OwnerReferences: []v1.OwnerReference{
{ {
APIVersion: nbResource.APIVersion, APIVersion: netbirdiov1.GroupVersion.Identifier(),
Kind: nbResource.Kind, Kind: "NBResource",
Name: nbResource.Name, Name: nbResource.Name,
UID: nbResource.UID, UID: nbResource.UID,
BlockOwnerDeletion: util.Ptr(true), BlockOwnerDeletion: util.Ptr(true),
@@ -315,8 +372,8 @@ func (r *NBResourceReconciler) handleGroups(ctx context.Context, req ctrl.Reques
if !ownerExists { if !ownerExists {
nbGroup.OwnerReferences = append(nbGroup.OwnerReferences, v1.OwnerReference{ nbGroup.OwnerReferences = append(nbGroup.OwnerReferences, v1.OwnerReference{
APIVersion: nbResource.APIVersion, APIVersion: netbirdiov1.GroupVersion.Identifier(),
Kind: nbResource.Kind, Kind: "NBResource",
Name: nbResource.Name, Name: nbResource.Name,
UID: nbResource.UID, UID: nbResource.UID,
BlockOwnerDeletion: util.Ptr(true), BlockOwnerDeletion: util.Ptr(true),
@@ -380,8 +437,24 @@ func (r *NBResourceReconciler) handleDelete(ctx context.Context, req ctrl.Reques
} }
for _, g := range nbGroupList.Items { for _, g := range nbGroupList.Items {
// TODO: Handle multiple owners ownerIndex := -1
if len(g.OwnerReferences) > 0 && g.OwnerReferences[0].UID == nbResource.UID { for idx, o := range g.OwnerReferences {
if o.UID == nbResource.UID {
ownerIndex = idx
break
}
}
if ownerIndex == -1 {
continue
}
if len(g.OwnerReferences) > 1 {
g.OwnerReferences = slices.Delete(g.OwnerReferences, ownerIndex, ownerIndex+1)
err = r.Client.Update(ctx, &g)
if err != nil && !errors.IsNotFound(err) {
logger.Error(errKubernetesAPI, "error updating NBGroup", "err", err)
return err
}
} else if len(g.OwnerReferences) == 1 {
g.Finalizers = util.Without(g.Finalizers, "netbird.io/resource-cleanup") g.Finalizers = util.Without(g.Finalizers, "netbird.io/resource-cleanup")
err = r.Client.Update(ctx, &g) err = r.Client.Update(ctx, &g)
if err != nil && !errors.IsNotFound(err) { if err != nil && !errors.IsNotFound(err) {
+659 -22
View File
@@ -2,16 +2,25 @@ package controller
import ( import (
"context" "context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"github.com/go-logr/logr"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/reconcile"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1" netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1"
"github.com/netbirdio/kubernetes-operator/internal/util"
netbird "github.com/netbirdio/netbird/management/client/rest"
"github.com/netbirdio/netbird/management/server/http/api"
) )
var _ = Describe("NBResource Controller", func() { var _ = Describe("NBResource Controller", func() {
@@ -22,48 +31,676 @@ var _ = Describe("NBResource Controller", func() {
typeNamespacedName := types.NamespacedName{ typeNamespacedName := types.NamespacedName{
Name: resourceName, Name: resourceName,
Namespace: "default", // TODO(user):Modify as needed Namespace: "default",
} }
nbresource := &netbirdiov1.NBResource{} nbresource := &netbirdiov1.NBResource{}
var netbirdClient *netbird.Client
var mux *http.ServeMux
var server *httptest.Server
var controllerReconciler *NBResourceReconciler
BeforeEach(func() { BeforeEach(func() {
Skip("Not implemented yet") ctrl.SetLogger(logr.New(GinkgoLogr.GetSink()))
mux = &http.ServeMux{}
server = httptest.NewServer(mux)
netbirdClient = netbird.New(server.URL, "ABC")
controllerReconciler = &NBResourceReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
netbird: netbirdClient,
}
By("creating the custom resource for the Kind NBResource") By("creating the custom resource for the Kind NBResource")
err := k8sClient.Get(ctx, typeNamespacedName, nbresource) err := k8sClient.Get(ctx, typeNamespacedName, nbresource)
if err != nil && errors.IsNotFound(err) { if err != nil && errors.IsNotFound(err) {
resource := &netbirdiov1.NBResource{ nbresource = &netbirdiov1.NBResource{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: resourceName, Name: resourceName,
Namespace: "default", Namespace: "default",
Finalizers: []string{"netbird.io/cleanup"},
},
Spec: netbirdiov1.NBResourceSpec{
Name: "Test",
NetworkID: "test",
Address: "test.default.svc.cluster.local",
Groups: []string{"meow"},
TCPPorts: []int32{80},
}, },
// TODO(user): Specify other spec details if needed.
} }
Expect(k8sClient.Create(ctx, resource)).To(Succeed()) Expect(k8sClient.Create(ctx, nbresource)).To(Succeed())
} }
}) })
AfterEach(func() { AfterEach(func() {
// TODO(user): Cleanup logic after each test, like removing the resource instance.
resource := &netbirdiov1.NBResource{} resource := &netbirdiov1.NBResource{}
err := k8sClient.Get(ctx, typeNamespacedName, resource) err := k8sClient.Get(ctx, typeNamespacedName, resource)
Expect(err).NotTo(HaveOccurred()) if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
By("Cleanup the specific resource instance NBResource") if len(resource.Finalizers) > 0 {
Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) resource.Finalizers = nil
}) Expect(k8sClient.Update(ctx, resource)).To(Succeed())
It("should successfully reconcile the resource", func() { }
By("Reconciling the created resource")
controllerReconciler := &NBResourceReconciler{ By("Cleanup the specific resource instance NBResource")
Client: k8sClient, Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
Scheme: k8sClient.Scheme(),
} }
})
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ BeforeEach(func() {
NamespacedName: typeNamespacedName, mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) {
defer GinkgoRecover()
resp := []api.Group{
{
Id: "test",
Name: "meow",
},
}
bs, err := json.Marshal(resp)
Expect(err).NotTo(HaveOccurred())
_, err = w.Write(bs)
Expect(err).NotTo(HaveOccurred())
})
})
When("Network Resource doesn't exist", Ordered, func() {
AfterAll(func() {
nbGroup := &netbirdiov1.NBGroup{}
err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "meow"}, nbGroup)
if !errors.IsNotFound(err) {
if len(nbGroup.Finalizers) > 0 {
nbGroup.Finalizers = nil
Expect(k8sClient.Update(ctx, nbGroup)).To(Succeed())
}
Expect(k8sClient.Delete(ctx, nbGroup)).To(Succeed())
}
})
It("should create NBGroups", func() {
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
nbGroup := &netbirdiov1.NBGroup{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "meow"}, nbGroup)).To(Succeed())
nbGroup.Status.GroupID = util.Ptr("test")
Expect(k8sClient.Status().Update(ctx, nbGroup)).To(Succeed())
})
It("should create Network Resource", func() {
networkResourceCreated := false
mux.HandleFunc("/api/networks/test/resources", func(w http.ResponseWriter, r *http.Request) {
defer GinkgoRecover()
if r.Method == http.MethodPost {
networkResourceCreated = true
bs, err := io.ReadAll(r.Body)
Expect(err).NotTo(HaveOccurred())
var req api.PostApiNetworksNetworkIdResourcesJSONRequestBody
Expect(json.Unmarshal(bs, &req)).To(Succeed())
Expect(req.Name).To(Equal("Test"))
Expect(req.Description).NotTo(BeNil())
Expect(*req.Description).To(BeEquivalentTo("Created by kubernetes-operator"))
Expect(req.Enabled).To(BeTrue())
Expect(req.Groups).To(ConsistOf([]string{"test"}))
Expect(req.Address).To(Equal(nbresource.Spec.Address))
resp := api.NetworkResource{
Address: req.Address,
Description: req.Description,
Enabled: req.Enabled,
Groups: []api.GroupMinimum{
{
Id: "test",
Name: "meow",
},
},
Id: "test",
Name: req.Name,
Type: api.NetworkResourceTypeDomain,
}
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())
Expect(networkResourceCreated).To(BeTrue())
})
})
When("Network Resource exists", func() {
BeforeEach(func() {
nbresource.Status.NetworkResourceID = util.Ptr("test")
Expect(k8sClient.Status().Update(ctx, nbresource)).To(Succeed())
nbGroup := &netbirdiov1.NBGroup{
ObjectMeta: metav1.ObjectMeta{
Name: "meow",
Namespace: "default",
Finalizers: []string{"netbird.io/resource-cleanup"},
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: netbirdiov1.GroupVersion.Identifier(),
Kind: "NBResource",
Name: "test-resource",
UID: nbresource.UID,
},
},
},
Spec: netbirdiov1.NBGroupSpec{
Name: "meow",
},
}
Expect(k8sClient.Create(ctx, nbGroup)).To(Succeed())
nbGroup.Status.GroupID = util.Ptr("test")
Expect(k8sClient.Status().Update(ctx, nbGroup)).To(Succeed())
})
AfterEach(func() {
nbGroup := &netbirdiov1.NBGroup{}
err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "meow"}, nbGroup)
if !errors.IsNotFound(err) {
if len(nbGroup.Finalizers) > 0 {
nbGroup.Finalizers = nil
Expect(k8sClient.Update(ctx, nbGroup)).To(Succeed())
}
Expect(k8sClient.Delete(ctx, nbGroup)).To(Succeed())
}
})
When("Network Resource is out of date", func() {
It("should update Network Resource", func() {
resourceUpdated := false
mux.HandleFunc("/api/networks/test/resources/test", func(w http.ResponseWriter, r *http.Request) {
defer GinkgoRecover()
if r.Method == http.MethodGet {
resp := api.NetworkResource{
Address: nbresource.Spec.Address,
Description: &networkDescription,
Enabled: false,
Groups: []api.GroupMinimum{
{
Id: "test",
Name: "meow",
},
{
Id: "test2",
Name: "meow2",
},
},
Id: "test",
Name: nbresource.Spec.Name,
Type: api.NetworkResourceTypeDomain,
}
bs, err := json.Marshal(resp)
Expect(err).NotTo(HaveOccurred())
_, err = w.Write(bs)
Expect(err).NotTo(HaveOccurred())
} else if r.Method == http.MethodPut {
resourceUpdated = true
bs, err := io.ReadAll(r.Body)
Expect(err).NotTo(HaveOccurred())
var req api.PutApiNetworksNetworkIdResourcesResourceIdJSONRequestBody
Expect(json.Unmarshal(bs, &req)).To(Succeed())
Expect(req.Name).To(Equal("Test"))
Expect(req.Description).NotTo(BeNil())
Expect(*req.Description).To(BeEquivalentTo("Created by kubernetes-operator"))
Expect(req.Enabled).To(BeTrue())
Expect(req.Groups).To(ConsistOf([]string{"test"}))
Expect(req.Address).To(Equal(nbresource.Spec.Address))
resp := api.NetworkResource{
Address: nbresource.Spec.Address,
Description: &networkDescription,
Enabled: true,
Groups: []api.GroupMinimum{
{
Id: "test",
Name: "meow",
},
},
Id: "test",
Name: nbresource.Spec.Name,
Type: api.NetworkResourceTypeDomain,
}
bs, err = json.Marshal(resp)
Expect(err).NotTo(HaveOccurred())
_, err = w.Write(bs)
Expect(err).NotTo(HaveOccurred())
}
})
nbresource.Status.NetworkResourceID = util.Ptr("test")
Expect(k8sClient.Status().Update(ctx, nbresource)).To(Succeed())
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
Expect(resourceUpdated).To(BeTrue())
})
})
When("Network Resource is up-to-date", func() {
BeforeEach(func() {
mux.HandleFunc("/api/networks/test/resources/test", func(w http.ResponseWriter, r *http.Request) {
defer GinkgoRecover()
if r.Method == http.MethodGet {
resp := api.NetworkResource{
Address: nbresource.Spec.Address,
Description: &networkDescription,
Enabled: true,
Groups: []api.GroupMinimum{
{
Id: "test",
Name: "meow",
},
},
Id: "test",
Name: nbresource.Spec.Name,
Type: api.NetworkResourceTypeDomain,
}
bs, err := json.Marshal(resp)
Expect(err).NotTo(HaveOccurred())
_, err = w.Write(bs)
Expect(err).NotTo(HaveOccurred())
}
})
})
When("Policy is specified", Ordered, func() {
BeforeAll(func() {
nbPolicy := &netbirdiov1.NBPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "test-a",
},
Spec: netbirdiov1.NBPolicySpec{
Name: "Test A",
SourceGroups: []string{"All"},
},
}
Expect(k8sClient.Create(ctx, nbPolicy)).To(Succeed())
nbPolicy = &netbirdiov1.NBPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "test-b",
},
Spec: netbirdiov1.NBPolicySpec{
Name: "Test B",
SourceGroups: []string{"All"},
},
}
Expect(k8sClient.Create(ctx, nbPolicy)).To(Succeed())
})
AfterAll(func() {
nbPolicy := &netbirdiov1.NBPolicy{}
err := k8sClient.Get(ctx, types.NamespacedName{Name: "test-a"}, nbPolicy)
if !errors.IsNotFound(err) {
Expect(k8sClient.Delete(ctx, nbPolicy)).To(Succeed())
}
nbPolicy = &netbirdiov1.NBPolicy{}
err = k8sClient.Get(ctx, types.NamespacedName{Name: "test-b"}, nbPolicy)
if !errors.IsNotFound(err) {
Expect(k8sClient.Delete(ctx, nbPolicy)).To(Succeed())
}
})
It("should update policy status", func() {
nbresource.Spec.PolicyName = "test-a"
Expect(k8sClient.Update(ctx, nbresource)).To(Succeed())
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
nbPolicy := &netbirdiov1.NBPolicy{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "test-a"}, nbPolicy)).To(Succeed())
Expect(nbPolicy.Status.ManagedServiceList).To(ContainElement("default/test-resource"))
})
When("Policy is updated", func() {
It("should remove old reference and add new reference", func() {
nbresource.Spec.PolicyName = "test-b"
Expect(k8sClient.Update(ctx, nbresource)).To(Succeed())
nbresource.Status.PolicyName = util.Ptr("test-a")
Expect(k8sClient.Status().Update(ctx, nbresource)).To(Succeed())
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
nbPolicy := &netbirdiov1.NBPolicy{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "test-a"}, nbPolicy)).To(Succeed())
Expect(nbPolicy.Status.ManagedServiceList).NotTo(ContainElement("default/test-resource"))
nbPolicy = &netbirdiov1.NBPolicy{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "test-b"}, nbPolicy)).To(Succeed())
Expect(nbPolicy.Status.ManagedServiceList).To(ContainElement("default/test-resource"))
})
})
When("Policy is removed", func() {
It("should remove old reference", func() {
nbPolicy := &netbirdiov1.NBPolicy{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "test-a"}, nbPolicy)).To(Succeed())
nbPolicy.Status.ManagedServiceList = []string{"default/test-resource"}
Expect(k8sClient.Status().Update(ctx, nbPolicy)).To(Succeed())
nbresource.Spec.PolicyName = ""
Expect(k8sClient.Update(ctx, nbresource)).To(Succeed())
nbresource.Status.PolicyName = util.Ptr("test-a")
Expect(k8sClient.Status().Update(ctx, nbresource)).To(Succeed())
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
nbPolicy = &netbirdiov1.NBPolicy{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "test-a"}, nbPolicy)).To(Succeed())
Expect(nbPolicy.Status.ManagedServiceList).NotTo(ContainElement("default/test-resource"))
})
})
})
When("Groups are changed", func() {
When("Removed groups are no longer referenced by anything", func() {
It("should only remove finalizer", func() {
nbresource.Spec.Groups = []string{"meow2"}
Expect(k8sClient.Update(ctx, nbresource)).To(Succeed())
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
nbGroup := &netbirdiov1.NBGroup{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "meow"}, nbGroup)).To(Succeed())
Expect(nbGroup.Finalizers).To(BeEmpty())
Expect(nbGroup.OwnerReferences).To(HaveLen(1))
})
})
When("Removed groups are referenced by something else", func() {
BeforeEach(func() {
otherResource := &netbirdiov1.NBResource{
ObjectMeta: metav1.ObjectMeta{
Name: "not-test",
Namespace: "default",
},
Spec: netbirdiov1.NBResourceSpec{
Name: "nottest",
NetworkID: "test",
Address: "test",
Groups: []string{"test"},
},
}
Expect(k8sClient.Create(ctx, otherResource)).To(Succeed())
nbGroup := &netbirdiov1.NBGroup{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "meow"}, nbGroup)).To(Succeed())
nbGroup.OwnerReferences = append(nbGroup.OwnerReferences, metav1.OwnerReference{
APIVersion: "netbird.io/v1",
Kind: "NBResource",
Name: "not-test",
UID: otherResource.UID,
})
Expect(k8sClient.Update(ctx, nbGroup)).To(Succeed())
})
AfterEach(func() {
otherResource := &netbirdiov1.NBResource{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "not-test"}, otherResource)).To(Succeed())
Expect(k8sClient.Delete(ctx, otherResource)).To(Succeed())
})
It("should only remove owner reference", func() {
nbresource.Spec.Groups = []string{"meow2"}
Expect(k8sClient.Update(ctx, nbresource)).To(Succeed())
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
nbGroup := &netbirdiov1.NBGroup{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "meow"}, nbGroup)).To(Succeed())
Expect(nbGroup.Finalizers).To(HaveLen(1))
Expect(nbGroup.OwnerReferences).To(HaveLen(1))
Expect(nbGroup.OwnerReferences[0].Name).To(Equal("not-test"))
})
})
When("New groups are added", func() {
It("should create new groups", func() {
nbresource.Spec.Groups = []string{"meow", "meow3"}
Expect(k8sClient.Update(ctx, nbresource)).To(Succeed())
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
nbGroup := &netbirdiov1.NBGroup{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "meow3"}, nbGroup)).To(Succeed())
Expect(nbGroup.OwnerReferences).To(HaveLen(1))
Expect(nbGroup.Finalizers).To(ConsistOf([]string{"netbird.io/group-cleanup", "netbird.io/resource-cleanup"}))
})
})
})
})
When("Network Resource is removed from NetBird", func() {
BeforeEach(func() {
mux.HandleFunc("/api/networks/test/resources/test", func(w http.ResponseWriter, r *http.Request) {
defer GinkgoRecover()
if r.Method == http.MethodGet {
w.WriteHeader(404)
_, err := w.Write([]byte(`{"message": "not found", "code": 404}`))
Expect(err).NotTo(HaveOccurred())
}
})
})
It("should remove network resource ID and requeue", func() {
res, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
Expect(res.Requeue).To(BeTrue())
Expect(k8sClient.Get(ctx, typeNamespacedName, nbresource)).To(Succeed())
Expect(nbresource.Status.NetworkResourceID).To(BeNil())
})
})
})
When("NBResource is set for deletion", Ordered, func() {
BeforeAll(func() {
nbresource.Spec.Groups = []string{"meow", "meowdelete"}
Expect(k8sClient.Update(ctx, nbresource)).To(Succeed())
nbresource.Status.Groups = []string{"test", "testdelete"}
nbresource.Status.PolicyName = util.Ptr("test")
nbresource.Status.NetworkResourceID = util.Ptr("test")
Expect(k8sClient.Status().Update(ctx, nbresource)).To(Succeed())
nbPolicy := &netbirdiov1.NBPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
Spec: netbirdiov1.NBPolicySpec{
Name: "Test",
SourceGroups: []string{"All"},
Bidirectional: true,
},
}
Expect(k8sClient.Create(ctx, nbPolicy)).To(Succeed())
nbPolicy.Status.ManagedServiceList = []string{"default/test-resource"}
Expect(k8sClient.Status().Update(ctx, nbPolicy)).To(Succeed())
nbGroup := &netbirdiov1.NBGroup{
ObjectMeta: metav1.ObjectMeta{
Name: "meow",
Namespace: "default",
Finalizers: []string{"netbird.io/resource-cleanup"},
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: netbirdiov1.GroupVersion.Identifier(),
Kind: "NBResource",
Name: nbresource.Name,
UID: nbresource.UID,
},
},
},
Spec: netbirdiov1.NBGroupSpec{
Name: "meow",
},
}
Expect(k8sClient.Create(ctx, nbGroup)).To(Succeed())
othernbresource := &netbirdiov1.NBResource{
ObjectMeta: metav1.ObjectMeta{
Name: "other-resource",
Namespace: "default",
},
Spec: netbirdiov1.NBResourceSpec{
Name: "test",
NetworkID: "test",
Address: "test",
Groups: []string{"test"},
},
}
Expect(k8sClient.Create(ctx, othernbresource)).To(Succeed())
nbGroup = &netbirdiov1.NBGroup{
ObjectMeta: metav1.ObjectMeta{
Name: "meowdelete",
Namespace: "default",
Finalizers: []string{"netbird.io/resource-cleanup"},
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: netbirdiov1.GroupVersion.Identifier(),
Kind: "NBResource",
Name: nbresource.Name,
UID: nbresource.UID,
},
{
APIVersion: netbirdiov1.GroupVersion.Identifier(),
Kind: "NBResource",
Name: othernbresource.Name,
UID: othernbresource.UID,
},
},
},
Spec: netbirdiov1.NBGroupSpec{
Name: "meow",
},
}
Expect(k8sClient.Create(ctx, nbGroup)).To(Succeed())
})
AfterAll(func() {
policy := &netbirdiov1.NBPolicy{}
err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "test"}, policy)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
if len(policy.Finalizers) > 0 {
policy.Finalizers = nil
Expect(k8sClient.Update(ctx, policy)).To(Succeed())
}
Expect(k8sClient.Delete(ctx, policy)).To(Succeed())
}
resource := &netbirdiov1.NBResource{}
err = k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "other-resource"}, resource)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
if len(resource.Finalizers) > 0 {
resource.Finalizers = nil
Expect(k8sClient.Update(ctx, resource)).To(Succeed())
}
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
}
group := &netbirdiov1.NBGroup{}
err = k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "meow"}, group)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
if len(group.Finalizers) > 0 {
group.Finalizers = nil
Expect(k8sClient.Update(ctx, group)).To(Succeed())
}
Expect(k8sClient.Delete(ctx, group)).To(Succeed())
}
group = &netbirdiov1.NBGroup{}
err = k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "meowdelete"}, group)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
if len(group.Finalizers) > 0 {
group.Finalizers = nil
Expect(k8sClient.Update(ctx, group)).To(Succeed())
}
Expect(k8sClient.Delete(ctx, group)).To(Succeed())
}
})
It("should delete Network Resource", func() {
Expect(k8sClient.Delete(ctx, nbresource)).To(Succeed())
resourceDeleted := false
mux.HandleFunc("/api/networks/test/resources/test", func(w http.ResponseWriter, r *http.Request) {
defer GinkgoRecover()
if r.Method == http.MethodDelete {
resourceDeleted = true
_, err := w.Write([]byte(`{}`))
Expect(err).NotTo(HaveOccurred())
}
})
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
Expect(resourceDeleted).To(BeTrue())
err = k8sClient.Get(ctx, typeNamespacedName, nbresource)
Expect(errors.IsNotFound(err)).To(BeTrue())
})
It("should remove resource cleanup finalizer from solely-owned NBGroups", func() {
group := &netbirdiov1.NBGroup{}
err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "meow"}, group)
if errors.IsNotFound(err) {
return
}
Expect(group.Finalizers).To(BeEmpty())
})
It("should remove owner reference from shared NBGroups", func() {
group := &netbirdiov1.NBGroup{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "meowdelete"}, group)).To(Succeed())
Expect(group.Finalizers).To(HaveLen(1))
Expect(group.OwnerReferences).To(HaveLen(1))
})
It("should remove policy reference", func() {
policy := &netbirdiov1.NBPolicy{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "test"}, policy)).To(Succeed())
Expect(policy.Status.ManagedServiceList).NotTo(ContainElement("default/test-resource"))
}) })
Expect(err).NotTo(HaveOccurred())
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
// Example: If you expect a certain status condition after reconciliation, verify it here.
}) })
}) })
}) })
+22 -13
View File
@@ -51,10 +51,10 @@ func (r *NBRoutingPeerReconciler) Reconcile(ctx context.Context, req ctrl.Reques
originalNBRP := nbrp.DeepCopy() originalNBRP := nbrp.DeepCopy()
defer func() { defer func() {
if originalNBRP.Status.NetworkID != nbrp.Status.NetworkID || if originalNBRP.DeletionTimestamp != nil && len(nbrp.Finalizers) == 0 {
originalNBRP.Status.RouterID != nbrp.Status.RouterID || return
originalNBRP.Status.SetupKeyID != nbrp.Status.SetupKeyID || }
!util.Equivalent(originalNBRP.Status.Conditions, nbrp.Status.Conditions) { if !originalNBRP.Status.Equal(nbrp.Status) {
err = r.Client.Status().Update(ctx, nbrp) err = r.Client.Status().Update(ctx, nbrp)
if err != nil { if err != nil {
logger.Error(errKubernetesAPI, "error updating NBRoutingPeer Status", "err", err) logger.Error(errKubernetesAPI, "error updating NBRoutingPeer Status", "err", err)
@@ -128,8 +128,8 @@ func (r *NBRoutingPeerReconciler) handleDeployment(ctx context.Context, req ctrl
Namespace: nbrp.Namespace, Namespace: nbrp.Namespace,
OwnerReferences: []v1.OwnerReference{ OwnerReferences: []v1.OwnerReference{
{ {
APIVersion: nbrp.APIVersion, APIVersion: netbirdiov1.GroupVersion.Identifier(),
Kind: nbrp.Kind, Kind: "NBRoutingPeer",
Name: nbrp.Name, Name: nbrp.Name,
UID: nbrp.UID, UID: nbrp.UID,
BlockOwnerDeletion: util.Ptr(true), BlockOwnerDeletion: util.Ptr(true),
@@ -199,8 +199,8 @@ func (r *NBRoutingPeerReconciler) handleDeployment(ctx context.Context, req ctrl
updatedDeployment.ObjectMeta.Namespace = nbrp.Namespace updatedDeployment.ObjectMeta.Namespace = nbrp.Namespace
updatedDeployment.ObjectMeta.OwnerReferences = []v1.OwnerReference{ updatedDeployment.ObjectMeta.OwnerReferences = []v1.OwnerReference{
{ {
APIVersion: nbrp.APIVersion, APIVersion: netbirdiov1.GroupVersion.Identifier(),
Kind: nbrp.Kind, Kind: "NBRoutingPeer",
Name: nbrp.Name, Name: nbrp.Name,
UID: nbrp.UID, UID: nbrp.UID,
BlockOwnerDeletion: util.Ptr(true), BlockOwnerDeletion: util.Ptr(true),
@@ -355,8 +355,8 @@ func (r *NBRoutingPeerReconciler) handleSetupKey(ctx context.Context, req ctrl.R
Namespace: nbrp.Namespace, Namespace: nbrp.Namespace,
OwnerReferences: []v1.OwnerReference{ OwnerReferences: []v1.OwnerReference{
{ {
APIVersion: nbrp.APIVersion, APIVersion: netbirdiov1.GroupVersion.Identifier(),
Kind: nbrp.Kind, Kind: "NBRoutingPeer",
Name: nbrp.Name, Name: nbrp.Name,
UID: nbrp.UID, UID: nbrp.UID,
BlockOwnerDeletion: util.Ptr(true), BlockOwnerDeletion: util.Ptr(true),
@@ -387,8 +387,17 @@ func (r *NBRoutingPeerReconciler) handleSetupKey(ctx context.Context, req ctrl.R
} }
if (err != nil && strings.Contains(err.Error(), "not found")) || setupKey.Revoked { if (err != nil && strings.Contains(err.Error(), "not found")) || setupKey.Revoked {
nbrp.Status.SetupKeyID = nil if setupKey != nil && setupKey.Revoked {
err = r.netbird.SetupKeys.Delete(ctx, *nbrp.Status.SetupKeyID)
if err != nil {
logger.Error(errNetBirdAPI, "error deleting setup key", "err", err)
nbrp.Status.Conditions = netbirdiov1.NBConditionFalse("APIError", fmt.Sprintf("error deleting setup key: %v", err))
return &ctrl.Result{}, err
}
}
nbrp.Status.SetupKeyID = nil
// Requeue to avoid repeating code // Requeue to avoid repeating code
return &ctrl.Result{Requeue: true}, nil return &ctrl.Result{Requeue: true}, nil
} }
@@ -447,8 +456,8 @@ func (r *NBRoutingPeerReconciler) handleGroup(ctx context.Context, req ctrl.Requ
Namespace: nbrp.Namespace, Namespace: nbrp.Namespace,
OwnerReferences: []v1.OwnerReference{ OwnerReferences: []v1.OwnerReference{
{ {
APIVersion: nbrp.APIVersion, APIVersion: netbirdiov1.GroupVersion.Identifier(),
Kind: nbrp.Kind, Kind: "NBRoutingPeer",
Name: nbrp.Name, Name: nbrp.Name,
UID: nbrp.UID, UID: nbrp.UID,
BlockOwnerDeletion: util.Ptr(true), BlockOwnerDeletion: util.Ptr(true),
File diff suppressed because it is too large Load Diff
+57 -41
View File
@@ -157,7 +157,8 @@ func (r *ServiceReconciler) exposeService(ctx context.Context, req ctrl.Request,
return ctrl.Result{}, err return ctrl.Result{}, err
} }
nbrsErr := r.reconcileNBResource(&nbResource, req, svc, routingPeer) originalNBResource := nbResource.DeepCopy()
nbrsErr := r.reconcileNBResource(&nbResource, req, svc, routingPeer, logger)
if nbrsErr != nil { if nbrsErr != nil {
return ctrl.Result{}, nbrsErr return ctrl.Result{}, nbrsErr
} }
@@ -168,7 +169,7 @@ func (r *ServiceReconciler) exposeService(ctx context.Context, req ctrl.Request,
logger.Error(errKubernetesAPI, "error creating NBResource", "err", err) logger.Error(errKubernetesAPI, "error creating NBResource", "err", err)
return ctrl.Result{}, err return ctrl.Result{}, err
} }
} else { } else if !originalNBResource.Spec.Equal(nbResource.Spec) {
err = r.Client.Update(ctx, &nbResource) err = r.Client.Update(ctx, &nbResource)
if err != nil { if err != nil {
logger.Error(errKubernetesAPI, "error updating NBResource", "err", err) logger.Error(errKubernetesAPI, "error updating NBResource", "err", err)
@@ -180,7 +181,7 @@ func (r *ServiceReconciler) exposeService(ctx context.Context, req ctrl.Request,
} }
// reconcileNBResource ensures NBResource settings are in-line with Service definition and annotations // reconcileNBResource ensures NBResource settings are in-line with Service definition and annotations
func (r *ServiceReconciler) reconcileNBResource(nbResource *netbirdiov1.NBResource, req ctrl.Request, svc corev1.Service, routingPeer netbirdiov1.NBRoutingPeer) error { func (r *ServiceReconciler) reconcileNBResource(nbResource *netbirdiov1.NBResource, req ctrl.Request, svc corev1.Service, routingPeer netbirdiov1.NBRoutingPeer, logger logr.Logger) error {
groups := []string{fmt.Sprintf("%s-%s-%s", r.ClusterName, req.Namespace, req.Name)} groups := []string{fmt.Sprintf("%s-%s-%s", r.ClusterName, req.Namespace, req.Name)}
if v, ok := svc.Annotations[serviceGroupsAnnotation]; ok { if v, ok := svc.Annotations[serviceGroupsAnnotation]; ok {
groups = nil groups = nil
@@ -202,50 +203,65 @@ func (r *ServiceReconciler) reconcileNBResource(nbResource *netbirdiov1.NBResour
nbResource.Spec.Address = fmt.Sprintf("%s.%s.%s", svc.Name, svc.Namespace, r.ClusterDNS) nbResource.Spec.Address = fmt.Sprintf("%s.%s.%s", svc.Name, svc.Namespace, r.ClusterDNS)
nbResource.Spec.Groups = groups nbResource.Spec.Groups = groups
if v, ok := svc.Annotations[servicePolicyAnnotation]; ok { if _, ok := svc.Annotations[servicePolicyAnnotation]; ok {
nbResource.Spec.PolicyName = v err := r.applyPolicy(nbResource, svc, logger)
var filterProtocols []string if err != nil {
if v, ok := svc.Annotations[serviceProtocolAnnotation]; ok { return err
filterProtocols = []string{v}
} }
var filterPorts []int32 } else if nbResource.Spec.PolicyName != "" {
if v, ok := svc.Annotations[servicePortsAnnotation]; ok { nbResource.Spec.PolicyName = ""
for _, v := range strings.Split(v, ",") { nbResource.Spec.TCPPorts = nil
port, err := strconv.ParseInt(v, 10, 64) nbResource.Spec.UDPPorts = nil
if err != nil { }
return err
}
filterPorts = append(filterPorts, int32(port)) return nil
} }
}
for _, p := range svc.Spec.Ports { func (r *ServiceReconciler) applyPolicy(nbResource *netbirdiov1.NBResource, svc corev1.Service, logger logr.Logger) error {
if len(filterProtocols) > 0 && !util.Contains(filterProtocols, string(p.Protocol)) { nbResource.Spec.PolicyName = svc.Annotations[servicePolicyAnnotation]
continue var filterProtocols []string
} if v, ok := svc.Annotations[serviceProtocolAnnotation]; ok {
if len(filterPorts) > 0 && !util.Contains(filterPorts, p.Port) { filterProtocols = []string{v}
continue }
} var filterPorts []int32
switch p.Protocol { if v, ok := svc.Annotations[servicePortsAnnotation]; ok {
case corev1.ProtocolSCTP: for _, v := range strings.Split(v, ",") {
if !util.Contains(nbResource.Spec.TCPPorts, p.Port) { port, err := strconv.ParseInt(v, 10, 64)
nbResource.Spec.TCPPorts = append(nbResource.Spec.TCPPorts, p.Port) if err != nil {
} return err
case corev1.ProtocolTCP:
if !util.Contains(nbResource.Spec.TCPPorts, p.Port) {
nbResource.Spec.TCPPorts = append(nbResource.Spec.TCPPorts, p.Port)
}
case corev1.ProtocolUDP:
if !util.Contains(nbResource.Spec.UDPPorts, p.Port) {
nbResource.Spec.UDPPorts = append(nbResource.Spec.UDPPorts, p.Port)
}
default:
return errUnknownProtocol
} }
filterPorts = append(filterPorts, int32(port))
}
}
for _, p := range svc.Spec.Ports {
switch p.Protocol {
case corev1.ProtocolTCP:
if (len(filterPorts) > 0 && !util.Contains(filterPorts, p.Port)) || (len(filterProtocols) > 0 && !util.Contains(filterProtocols, "tcp")) {
if util.Contains(nbResource.Spec.TCPPorts, p.Port) {
nbResource.Spec.TCPPorts = util.Without(nbResource.Spec.TCPPorts, p.Port)
}
continue
}
if !util.Contains(nbResource.Spec.TCPPorts, p.Port) {
nbResource.Spec.TCPPorts = append(nbResource.Spec.TCPPorts, p.Port)
}
case corev1.ProtocolUDP:
if (len(filterPorts) > 0 && !util.Contains(filterPorts, p.Port)) || (len(filterProtocols) > 0 && !util.Contains(filterProtocols, "udp")) {
if util.Contains(nbResource.Spec.UDPPorts, p.Port) {
nbResource.Spec.UDPPorts = util.Without(nbResource.Spec.UDPPorts, p.Port)
}
continue
}
if !util.Contains(nbResource.Spec.UDPPorts, p.Port) {
nbResource.Spec.UDPPorts = append(nbResource.Spec.UDPPorts, p.Port)
}
default:
logger.Info("Unsupported protocol %v", p.Protocol)
continue
} }
} }
// TODO: Handle removed policy name
return nil return nil
} }
+460 -4
View File
@@ -1,17 +1,473 @@
package controller package controller
import ( import (
netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1"
"github.com/netbirdio/kubernetes-operator/internal/util"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
) )
var _ = Describe("Service Controller", func() { var _ = Describe("Service Controller", func() {
Context("When reconciling a resource", func() { Context("When reconciling a resource", func() {
typeNamespacedName := types.NamespacedName{
Namespace: "default",
Name: "test-resource",
}
const policyName = "test"
var service *corev1.Service
It("should successfully reconcile the resource", func() { var controllerReconciler *ServiceReconciler
Skip("Not implemented yet")
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic. BeforeEach(func() {
// Example: If you expect a certain status condition after reconciliation, verify it here. service = &corev1.Service{
ObjectMeta: v1.ObjectMeta{
Name: "test-resource",
Namespace: "default",
},
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Name: "a",
Protocol: corev1.ProtocolTCP,
Port: 80,
TargetPort: intstr.FromInt(80),
},
{
Name: "b",
Protocol: corev1.ProtocolTCP,
Port: 443,
TargetPort: intstr.FromInt(443),
},
{
Name: "c",
Protocol: corev1.ProtocolUDP,
Port: 80,
TargetPort: intstr.FromInt(80),
},
{
Name: "d",
Protocol: corev1.ProtocolUDP,
Port: 443,
TargetPort: intstr.FromInt(443),
},
},
},
}
Expect(k8sClient.Create(ctx, service)).To(Succeed())
controllerReconciler = &ServiceReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
ClusterName: "kubernetes",
NamespacedNetworks: false,
ClusterDNS: "svc.cluster.local",
ControllerNamespace: "default",
}
})
AfterEach(func() {
svc := &corev1.Service{}
err := k8sClient.Get(ctx, typeNamespacedName, svc)
if !errors.IsNotFound(err) {
if len(svc.Finalizers) > 0 {
svc.Finalizers = nil
Expect(k8sClient.Update(ctx, svc)).To(Succeed())
}
err := k8sClient.Delete(ctx, svc)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
}
}
nbrp := &netbirdiov1.NBRoutingPeer{}
err = k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "router"}, nbrp)
if !errors.IsNotFound(err) {
if len(nbrp.Finalizers) > 0 {
nbrp.Finalizers = nil
Expect(k8sClient.Update(ctx, nbrp)).To(Succeed())
}
err := k8sClient.Delete(ctx, nbrp)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
}
}
nbResource := &netbirdiov1.NBResource{}
err = k8sClient.Get(ctx, typeNamespacedName, nbResource)
if !errors.IsNotFound(err) {
if len(nbResource.Finalizers) > 0 {
nbResource.Finalizers = nil
Expect(k8sClient.Update(ctx, nbResource)).To(Succeed())
}
err := k8sClient.Delete(ctx, nbResource)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
}
}
})
When("Service is not already exposed", func() {
When("Service should not be exposed", func() {
It("should change nothing", func() {
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
Expect(k8sClient.Get(ctx, typeNamespacedName, service)).To(Succeed())
Expect(service.Finalizers).To(BeEmpty())
nbResource := &netbirdiov1.NBResource{}
Expect(k8sClient.Get(ctx, typeNamespacedName, nbResource)).NotTo(Succeed())
})
})
When("NBRoutingPeer doesn't exist", func() {
BeforeEach(func() {
if service.Annotations == nil {
service.Annotations = make(map[string]string)
}
service.Annotations[ServiceExposeAnnotation] = "trueish"
Expect(k8sClient.Update(ctx, service)).To(Succeed())
})
It("should create NBRoutingPeer and requeue until network ID is available", func() {
res, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
Expect(res.RequeueAfter).NotTo(BeZero())
nbrp := &netbirdiov1.NBRoutingPeer{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: typeNamespacedName.Namespace, Name: "router"}, nbrp)).To(Succeed())
res, err = controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
Expect(res.RequeueAfter).NotTo(BeZero())
nbrp.Status.NetworkID = util.Ptr(policyName)
Expect(k8sClient.Status().Update(ctx, nbrp)).To(Succeed())
res, err = controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
Expect(res.RequeueAfter).To(BeZero())
})
})
When("NBRoutingPeer exists", func() {
BeforeEach(func() {
nbrp := &netbirdiov1.NBRoutingPeer{
ObjectMeta: v1.ObjectMeta{
Namespace: typeNamespacedName.Namespace,
Name: "router",
},
Spec: netbirdiov1.NBRoutingPeerSpec{},
}
Expect(k8sClient.Create(ctx, nbrp)).To(Succeed())
nbrp.Status.NetworkID = util.Ptr(policyName)
Expect(k8sClient.Status().Update(ctx, nbrp)).To(Succeed())
})
When("Service should be exposed", func() {
BeforeEach(func() {
if service.Annotations == nil {
service.Annotations = make(map[string]string)
}
service.Annotations[ServiceExposeAnnotation] = "true"
Expect(k8sClient.Update(ctx, service)).To(Succeed())
})
It("should add finalizer to service object", func() {
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
Expect(k8sClient.Get(ctx, typeNamespacedName, service)).To(Succeed())
Expect(service.Finalizers).To(ContainElement("netbird.io/cleanup"))
})
When("nothing else is specified", func() {
It("should create NBResource with default values", func() {
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
nbResource := &netbirdiov1.NBResource{}
Expect(k8sClient.Get(ctx, typeNamespacedName, nbResource)).To(Succeed())
Expect(nbResource.Spec.Address).To(Equal(typeNamespacedName.Name + "." + typeNamespacedName.Namespace + "." + controllerReconciler.ClusterDNS))
Expect(nbResource.Spec.Groups).To(ConsistOf([]string{controllerReconciler.ClusterName + "-" + typeNamespacedName.Namespace + "-" + typeNamespacedName.Name}))
Expect(nbResource.Spec.Name).To(Equal(typeNamespacedName.Namespace + "-" + typeNamespacedName.Name))
Expect(nbResource.Spec.NetworkID).To(Equal(policyName))
Expect(nbResource.Spec.PolicyName).To(BeEmpty())
Expect(nbResource.Spec.TCPPorts).To(BeEmpty())
Expect(nbResource.Spec.UDPPorts).To(BeEmpty())
})
})
When("policy is specified", func() {
BeforeEach(func() {
service.Annotations[servicePolicyAnnotation] = policyName
Expect(k8sClient.Update(ctx, service)).To(Succeed())
})
When("nothing is restricted", func() {
It("should create NBResource with policy", func() {
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
nbResource := &netbirdiov1.NBResource{}
Expect(k8sClient.Get(ctx, typeNamespacedName, nbResource)).To(Succeed())
Expect(nbResource.Spec.PolicyName).To(Equal(policyName))
Expect(nbResource.Spec.TCPPorts).To(ConsistOf([]int32{443, 80}))
Expect(nbResource.Spec.UDPPorts).To(ConsistOf([]int32{443, 80}))
})
})
When("ports are restricted", func() {
It("should create NBResource with policy and only specified ports", func() {
service.Annotations[servicePortsAnnotation] = "80"
Expect(k8sClient.Update(ctx, service)).To(Succeed())
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
nbResource := &netbirdiov1.NBResource{}
Expect(k8sClient.Get(ctx, typeNamespacedName, nbResource)).To(Succeed())
Expect(nbResource.Spec.PolicyName).To(Equal(policyName))
Expect(nbResource.Spec.TCPPorts).To(ConsistOf([]int32{80}))
Expect(nbResource.Spec.UDPPorts).To(ConsistOf([]int32{80}))
})
})
When("protocol is restricted", func() {
It("should create NBResource with policy and only specified protocol", func() {
service.Annotations[serviceProtocolAnnotation] = "tcp"
Expect(k8sClient.Update(ctx, service)).To(Succeed())
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
nbResource := &netbirdiov1.NBResource{}
Expect(k8sClient.Get(ctx, typeNamespacedName, nbResource)).To(Succeed())
Expect(nbResource.Spec.PolicyName).To(Equal(policyName))
Expect(nbResource.Spec.TCPPorts).To(ConsistOf([]int32{80, 443}))
Expect(nbResource.Spec.UDPPorts).To(BeEmpty())
})
})
})
When("resource name is specified", func() {
It("should create NBResource with specified name", func() {
service.Annotations[serviceResourceAnnotation] = "meow"
Expect(k8sClient.Update(ctx, service)).To(Succeed())
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
nbResource := &netbirdiov1.NBResource{}
Expect(k8sClient.Get(ctx, typeNamespacedName, nbResource)).To(Succeed())
Expect(nbResource.Spec.Name).To(Equal("meow"))
})
})
When("resource groups specified", func() {
It("should create NBResource with specified groups", func() {
service.Annotations[serviceGroupsAnnotation] = "meow, wow ,test"
Expect(k8sClient.Update(ctx, service)).To(Succeed())
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
nbResource := &netbirdiov1.NBResource{}
Expect(k8sClient.Get(ctx, typeNamespacedName, nbResource)).To(Succeed())
Expect(nbResource.Spec.Groups).To(ConsistOf([]string{"meow", "wow", policyName}))
})
})
})
})
})
When("Service is already exposed", func() {
BeforeEach(func() {
nbResource := &netbirdiov1.NBResource{
ObjectMeta: v1.ObjectMeta{
Name: typeNamespacedName.Name,
Namespace: typeNamespacedName.Namespace,
},
Spec: netbirdiov1.NBResourceSpec{
Name: typeNamespacedName.Namespace + "-" + typeNamespacedName.Name,
Address: typeNamespacedName.Name + "." + typeNamespacedName.Namespace + "." + controllerReconciler.ClusterDNS,
Groups: []string{controllerReconciler.ClusterName + "-" + typeNamespacedName.Namespace + "-" + typeNamespacedName.Name},
NetworkID: policyName,
},
}
Expect(k8sClient.Create(ctx, nbResource)).To(Succeed())
if service.Annotations == nil {
service.Annotations = make(map[string]string)
}
service.Annotations[ServiceExposeAnnotation] = "true"
Expect(k8sClient.Update(ctx, service)).To(Succeed())
nbrp := &netbirdiov1.NBRoutingPeer{
ObjectMeta: v1.ObjectMeta{
Namespace: typeNamespacedName.Namespace,
Name: "router",
},
Spec: netbirdiov1.NBRoutingPeerSpec{},
}
Expect(k8sClient.Create(ctx, nbrp)).To(Succeed())
nbrp.Status.NetworkID = util.Ptr(policyName)
Expect(k8sClient.Status().Update(ctx, nbrp)).To(Succeed())
})
When("Service should not be exposed", func() {
BeforeEach(func() {
delete(service.Annotations, ServiceExposeAnnotation)
Expect(k8sClient.Update(ctx, service)).To(Succeed())
})
It("should delete NBResource", func() {
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
nbResource := &netbirdiov1.NBResource{}
err = k8sClient.Get(ctx, typeNamespacedName, nbResource)
if !errors.IsNotFound(err) {
Expect(nbResource.DeletionTimestamp).NotTo(BeNil())
}
})
It("should remove finalizer from Service", func() {
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
Expect(k8sClient.Get(ctx, typeNamespacedName, service)).To(Succeed())
Expect(service.Finalizers).NotTo(ContainElement("netbird.io/cleanup"))
})
})
When("Nothing changes", func() {
It("should do nothing", func() {
nbResource := &netbirdiov1.NBResource{}
Expect(k8sClient.Get(ctx, typeNamespacedName, nbResource)).To(Succeed())
resourceVersion := nbResource.ResourceVersion
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
nbResource = &netbirdiov1.NBResource{}
Expect(k8sClient.Get(ctx, typeNamespacedName, nbResource)).To(Succeed())
Expect(resourceVersion).To(BeEquivalentTo(nbResource.ResourceVersion))
})
})
When("policy changes", func() {
It("should update policy in NBResource spec", func() {
service.Annotations[servicePolicyAnnotation] = policyName
Expect(k8sClient.Update(ctx, service)).To(Succeed())
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
nbResource := &netbirdiov1.NBResource{}
Expect(k8sClient.Get(ctx, typeNamespacedName, nbResource)).To(Succeed())
Expect(nbResource.Spec.PolicyName).To(Equal(policyName))
})
})
When("policy is removed", func() {
It("should remove policy in NBResource spec", func() {
nbResource := &netbirdiov1.NBResource{}
Expect(k8sClient.Get(ctx, typeNamespacedName, nbResource)).To(Succeed())
nbResource.Spec.PolicyName = policyName
Expect(k8sClient.Update(ctx, nbResource)).To(Succeed())
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
nbResource = &netbirdiov1.NBResource{}
Expect(k8sClient.Get(ctx, typeNamespacedName, nbResource)).To(Succeed())
Expect(nbResource.Spec.PolicyName).To(Equal(""))
})
})
When("policy ports changes", func() {
It("should update ports in NBResource spec", func() {
nbResource := &netbirdiov1.NBResource{}
Expect(k8sClient.Get(ctx, typeNamespacedName, nbResource)).To(Succeed())
nbResource.Spec.PolicyName = policyName
nbResource.Spec.TCPPorts = []int32{443, 80}
nbResource.Spec.UDPPorts = []int32{443, 80}
Expect(k8sClient.Update(ctx, nbResource)).To(Succeed())
service.Annotations[servicePolicyAnnotation] = policyName
service.Annotations[servicePortsAnnotation] = "80"
Expect(k8sClient.Update(ctx, service)).To(Succeed())
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
nbResource = &netbirdiov1.NBResource{}
Expect(k8sClient.Get(ctx, typeNamespacedName, nbResource)).To(Succeed())
Expect(nbResource.Spec.TCPPorts).To(ConsistOf([]int32{80}))
Expect(nbResource.Spec.UDPPorts).To(ConsistOf([]int32{80}))
})
})
When("policy protocol changes", func() {
It("should update protocol in NBResource spec", func() {
nbResource := &netbirdiov1.NBResource{}
Expect(k8sClient.Get(ctx, typeNamespacedName, nbResource)).To(Succeed())
nbResource.Spec.PolicyName = policyName
nbResource.Spec.TCPPorts = []int32{443, 80}
nbResource.Spec.UDPPorts = []int32{443, 80}
Expect(k8sClient.Update(ctx, nbResource)).To(Succeed())
service.Annotations[servicePolicyAnnotation] = policyName
service.Annotations[serviceProtocolAnnotation] = "tcp"
Expect(k8sClient.Update(ctx, service)).To(Succeed())
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
nbResource = &netbirdiov1.NBResource{}
Expect(k8sClient.Get(ctx, typeNamespacedName, nbResource)).To(Succeed())
Expect(nbResource.Spec.TCPPorts).To(ConsistOf([]int32{80, 443}))
Expect(nbResource.Spec.UDPPorts).To(BeEmpty())
})
})
When("resource name changes", func() {
It("should update name in NBResource spec", func() {
service.Annotations[serviceResourceAnnotation] = "meow"
Expect(k8sClient.Update(ctx, service)).To(Succeed())
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
nbResource := &netbirdiov1.NBResource{}
Expect(k8sClient.Get(ctx, typeNamespacedName, nbResource)).To(Succeed())
Expect(nbResource.Spec.Name).To(Equal("meow"))
})
})
When("resource groups changes", func() {
It("should update groups in NBResource spec", func() {
service.Annotations[serviceGroupsAnnotation] = "a7medmo7sen, pewpewpew"
Expect(k8sClient.Update(ctx, service)).To(Succeed())
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
nbResource := &netbirdiov1.NBResource{}
Expect(k8sClient.Get(ctx, typeNamespacedName, nbResource)).To(Succeed())
Expect(nbResource.Spec.Groups).To(ConsistOf([]string{"a7medmo7sen", "pewpewpew"}))
})
})
}) })
}) })
}) })
+3 -5
View File
@@ -14,7 +14,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook/admission" "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1" netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1"
netbird "github.com/netbirdio/netbird/management/client/rest"
) )
// nolint:unused // nolint:unused
@@ -22,17 +21,16 @@ import (
var nbgrouplog = logf.Log.WithName("nbgroup-resource") var nbgrouplog = logf.Log.WithName("nbgroup-resource")
// SetupNBGroupWebhookWithManager registers the webhook for NBGroup in the manager. // SetupNBGroupWebhookWithManager registers the webhook for NBGroup in the manager.
func SetupNBGroupWebhookWithManager(mgr ctrl.Manager, managementURL, apiKey string) error { func SetupNBGroupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).For(&netbirdiov1.NBGroup{}). return ctrl.NewWebhookManagedBy(mgr).For(&netbirdiov1.NBGroup{}).
WithValidator(&NBGroupCustomValidator{netbird: netbird.New(managementURL, apiKey), client: mgr.GetClient()}). WithValidator(&NBGroupCustomValidator{client: mgr.GetClient()}).
Complete() Complete()
} }
// NBGroupCustomValidator struct is responsible for validating the NBGroup resource // NBGroupCustomValidator struct is responsible for validating the NBGroup resource
// when it is created, updated, or deleted. // when it is created, updated, or deleted.
type NBGroupCustomValidator struct { type NBGroupCustomValidator struct {
netbird *netbird.Client client client.Client
client client.Client
} }
var _ webhook.CustomValidator = &NBGroupCustomValidator{} var _ webhook.CustomValidator = &NBGroupCustomValidator{}
+140 -25
View File
@@ -3,9 +3,11 @@ package v1
import ( import (
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1" netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1"
// TODO (user): Add any additional imports if needed
) )
var _ = Describe("NBGroup Webhook", func() { var _ = Describe("NBGroup Webhook", func() {
@@ -16,41 +18,154 @@ var _ = Describe("NBGroup Webhook", func() {
) )
BeforeEach(func() { BeforeEach(func() {
Skip("Not implemented yet")
obj = &netbirdiov1.NBGroup{} obj = &netbirdiov1.NBGroup{}
oldObj = &netbirdiov1.NBGroup{} oldObj = &netbirdiov1.NBGroup{}
validator = NBGroupCustomValidator{} validator = NBGroupCustomValidator{
client: k8sClient,
}
Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") Expect(validator).NotTo(BeNil(), "Expected validator to be initialized")
Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
// TODO (user): Add any setup logic common to all tests
}) })
AfterEach(func() { AfterEach(func() {
// TODO (user): Add any teardown logic common to all tests
}) })
Context("When creating or updating NBGroup under Validating Webhook", func() { Context("When creating or updating NBGroup under Validating Webhook", func() {
// TODO (user): Add logic for validating webhooks It("should allow creation", func() {
// Example: Expect(validator.ValidateCreate(ctx, obj)).Error().NotTo(HaveOccurred())
// It("Should deny creation if a required field is missing", func() { })
// By("simulating an invalid creation scenario") It("should allow update", func() {
// obj.SomeRequiredField = "" Expect(validator.ValidateUpdate(ctx, oldObj, obj)).Error().NotTo(HaveOccurred())
// Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred()) })
// }) When("There are no owners", func() {
// It("should allow deletion", func() {
// It("Should admit creation if all required fields are present", func() { obj = &netbirdiov1.NBGroup{
// By("simulating an invalid creation scenario") ObjectMeta: v1.ObjectMeta{
// obj.SomeRequiredField = "valid_value" Name: "test",
// Expect(validator.ValidateCreate(ctx, obj)).To(BeNil()) Namespace: "default",
// }) OwnerReferences: nil,
// },
// It("Should validate updates correctly", func() { }
// By("simulating a valid update scenario") Expect(validator.ValidateDelete(ctx, obj)).Error().NotTo(HaveOccurred())
// oldObj.SomeRequiredField = "updated_value" })
// obj.SomeRequiredField = "updated_value" })
// Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil()) When("There deleted owners", func() {
// }) It("should allow deletion", func() {
obj = &netbirdiov1.NBGroup{
ObjectMeta: v1.ObjectMeta{
Name: "test",
Namespace: "default",
OwnerReferences: []v1.OwnerReference{
{
APIVersion: netbirdiov1.GroupVersion.Identifier(),
Kind: "NBResource",
Name: "notexist",
UID: obj.UID,
},
},
},
}
Expect(validator.ValidateDelete(ctx, obj)).Error().NotTo(HaveOccurred())
})
})
When("NBResource owner exists", func() {
BeforeEach(func() {
nbResource := &netbirdiov1.NBResource{
ObjectMeta: v1.ObjectMeta{
Name: "isexist",
Namespace: "default",
},
Spec: netbirdiov1.NBResourceSpec{
Name: "test1",
NetworkID: "test2",
Address: "test3",
Groups: []string{"test"},
},
}
Expect(k8sClient.Create(ctx, nbResource)).To(Succeed())
obj = &netbirdiov1.NBGroup{
ObjectMeta: v1.ObjectMeta{
Name: "test",
Namespace: "default",
OwnerReferences: []v1.OwnerReference{
{
APIVersion: netbirdiov1.GroupVersion.Identifier(),
Kind: nbResource.Kind,
Name: nbResource.Name,
UID: nbResource.UID,
},
},
},
}
})
AfterEach(func() {
nbResource := &netbirdiov1.NBResource{}
err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "isexist"}, nbResource)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
if len(nbResource.Finalizers) > 0 {
nbResource.Finalizers = nil
Expect(k8sClient.Update(ctx, nbResource)).To(Succeed())
}
err = k8sClient.Delete(ctx, nbResource)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
}
}
})
It("should deny deletion", func() {
Expect(validator.ValidateDelete(ctx, obj)).Error().To(HaveOccurred())
})
})
When("NBRoutingPeer owner exists", func() {
BeforeEach(func() {
nbrp := &netbirdiov1.NBRoutingPeer{
ObjectMeta: v1.ObjectMeta{
Name: "isexist",
Namespace: "default",
},
Spec: netbirdiov1.NBRoutingPeerSpec{},
}
Expect(k8sClient.Create(ctx, nbrp)).To(Succeed())
obj = &netbirdiov1.NBGroup{
ObjectMeta: v1.ObjectMeta{
Name: "test",
Namespace: "default",
OwnerReferences: []v1.OwnerReference{
{
APIVersion: netbirdiov1.GroupVersion.Identifier(),
Kind: nbrp.Kind,
Name: nbrp.Name,
UID: nbrp.UID,
},
},
},
}
})
AfterEach(func() {
nbrp := &netbirdiov1.NBRoutingPeer{}
err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "isexist"}, nbrp)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
if len(nbrp.Finalizers) > 0 {
nbrp.Finalizers = nil
Expect(k8sClient.Update(ctx, nbrp)).To(Succeed())
}
err = k8sClient.Delete(ctx, nbrp)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
}
}
})
It("should deny deletion", func() {
Expect(validator.ValidateDelete(ctx, obj)).Error().To(HaveOccurred())
})
})
}) })
}) })
+7 -5
View File
@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime" ctrl "sigs.k8s.io/controller-runtime"
@@ -15,7 +16,6 @@ import (
netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1" netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1"
"github.com/netbirdio/kubernetes-operator/internal/controller" "github.com/netbirdio/kubernetes-operator/internal/controller"
netbird "github.com/netbirdio/netbird/management/client/rest"
) )
// nolint:unused // nolint:unused
@@ -23,17 +23,16 @@ import (
var nbresourcelog = logf.Log.WithName("nbresource-resource") var nbresourcelog = logf.Log.WithName("nbresource-resource")
// SetupNBResourceWebhookWithManager registers the webhook for NBResource in the manager. // SetupNBResourceWebhookWithManager registers the webhook for NBResource in the manager.
func SetupNBResourceWebhookWithManager(mgr ctrl.Manager, managementURL, apiKey string) error { func SetupNBResourceWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).For(&netbirdiov1.NBResource{}). return ctrl.NewWebhookManagedBy(mgr).For(&netbirdiov1.NBResource{}).
WithValidator(&NBResourceCustomValidator{netbird: netbird.New(managementURL, apiKey), client: mgr.GetClient()}). WithValidator(&NBResourceCustomValidator{client: mgr.GetClient()}).
Complete() Complete()
} }
// NBResourceCustomValidator struct is responsible for validating the NBResource resource // NBResourceCustomValidator struct is responsible for validating the NBResource resource
// when it is created, updated, or deleted. // when it is created, updated, or deleted.
type NBResourceCustomValidator struct { type NBResourceCustomValidator struct {
netbird *netbird.Client client client.Client
client client.Client
} }
var _ webhook.CustomValidator = &NBResourceCustomValidator{} var _ webhook.CustomValidator = &NBResourceCustomValidator{}
@@ -58,6 +57,9 @@ func (v *NBResourceCustomValidator) ValidateDelete(ctx context.Context, obj runt
var svc corev1.Service var svc corev1.Service
err := v.client.Get(ctx, types.NamespacedName{Namespace: nbresource.Namespace, Name: nbresource.Name}, &svc) err := v.client.Get(ctx, types.NamespacedName{Namespace: nbresource.Namespace, Name: nbresource.Name}, &svc)
if errors.IsNotFound(err) {
return nil, nil
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
+98 -32
View File
@@ -3,9 +3,13 @@ package v1
import ( import (
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1" netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1"
// TODO (user): Add any additional imports if needed
) )
var _ = Describe("NBResource Webhook", func() { var _ = Describe("NBResource Webhook", func() {
@@ -16,41 +20,103 @@ var _ = Describe("NBResource Webhook", func() {
) )
BeforeEach(func() { BeforeEach(func() {
Skip("Not implemented yet")
obj = &netbirdiov1.NBResource{} obj = &netbirdiov1.NBResource{}
oldObj = &netbirdiov1.NBResource{} oldObj = &netbirdiov1.NBResource{}
validator = NBResourceCustomValidator{} validator = NBResourceCustomValidator{
Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") client: k8sClient,
Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") }
Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
// TODO (user): Add any setup logic common to all tests
})
AfterEach(func() {
// TODO (user): Add any teardown logic common to all tests
}) })
Context("When creating or updating NBResource under Validating Webhook", func() { Context("When creating or updating NBResource under Validating Webhook", func() {
// TODO (user): Add logic for validating webhooks It("should allow creation", func() {
// Example: Expect(validator.ValidateCreate(ctx, obj)).Error().NotTo(HaveOccurred())
// It("Should deny creation if a required field is missing", func() { })
// By("simulating an invalid creation scenario") It("should allow update", func() {
// obj.SomeRequiredField = "" Expect(validator.ValidateUpdate(ctx, oldObj, obj)).Error().NotTo(HaveOccurred())
// Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred()) })
// }) When("No services are exposed", func() {
// BeforeEach(func() {
// It("Should admit creation if all required fields are present", func() { obj.Name = "maw"
// By("simulating an invalid creation scenario") obj.Namespace = "default"
// obj.SomeRequiredField = "valid_value" svc := &corev1.Service{
// Expect(validator.ValidateCreate(ctx, obj)).To(BeNil()) ObjectMeta: v1.ObjectMeta{
// }) Name: "ne",
// Namespace: "default",
// It("Should validate updates correctly", func() { },
// By("simulating a valid update scenario") Spec: corev1.ServiceSpec{
// oldObj.SomeRequiredField = "updated_value" Ports: []corev1.ServicePort{
// obj.SomeRequiredField = "updated_value" {
// Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil()) Protocol: corev1.ProtocolTCP,
// }) Port: 80,
}) TargetPort: intstr.FromInt32(80),
},
},
},
}
Expect(k8sClient.Create(ctx, svc)).To(Succeed())
})
AfterEach(func() {
svc := &netbirdiov1.NBResource{}
err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "ne"}, svc)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
if len(svc.Finalizers) > 0 {
svc.Finalizers = nil
Expect(k8sClient.Update(ctx, svc)).To(Succeed())
}
err = k8sClient.Delete(ctx, svc)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
}
}
})
It("should allow deletion", func() {
Expect(validator.ValidateDelete(ctx, obj)).Error().NotTo(HaveOccurred())
})
})
When("A service is exposed", func() {
BeforeEach(func() {
obj.Name = "maw"
obj.Namespace = "default"
svc := &corev1.Service{
ObjectMeta: v1.ObjectMeta{
Name: "maw",
Namespace: "default",
Annotations: map[string]string{
"netbird.io/expose": "true",
},
},
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Protocol: corev1.ProtocolTCP,
Port: 80,
TargetPort: intstr.FromInt32(80),
},
},
},
}
Expect(k8sClient.Create(ctx, svc)).To(Succeed())
})
AfterEach(func() {
svc := &corev1.Service{}
err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "maw"}, svc)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
if len(svc.Finalizers) > 0 {
svc.Finalizers = nil
Expect(k8sClient.Update(ctx, svc)).To(Succeed())
}
err = k8sClient.Delete(ctx, svc)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
}
}
})
It("should deny deletion", func() {
Expect(validator.ValidateDelete(ctx, obj)).Error().To(HaveOccurred())
})
})
})
}) })
+7 -7
View File
@@ -12,7 +12,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook/admission" "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1" netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1"
netbird "github.com/netbirdio/netbird/management/client/rest"
) )
// nolint:unused // nolint:unused
@@ -20,17 +19,16 @@ import (
var nbroutingpeerlog = logf.Log.WithName("nbroutingpeer-resource") var nbroutingpeerlog = logf.Log.WithName("nbroutingpeer-resource")
// SetupNBRoutingPeerWebhookWithManager registers the webhook for NBRoutingPeer in the manager. // SetupNBRoutingPeerWebhookWithManager registers the webhook for NBRoutingPeer in the manager.
func SetupNBRoutingPeerWebhookWithManager(mgr ctrl.Manager, managementURL, apiKey string) error { func SetupNBRoutingPeerWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).For(&netbirdiov1.NBRoutingPeer{}). return ctrl.NewWebhookManagedBy(mgr).For(&netbirdiov1.NBRoutingPeer{}).
WithValidator(&NBRoutingPeerCustomValidator{netbird: netbird.New(managementURL, apiKey), client: mgr.GetClient()}). WithValidator(&NBRoutingPeerCustomValidator{client: mgr.GetClient()}).
Complete() Complete()
} }
// NBRoutingPeerCustomValidator struct is responsible for validating the NBRoutingPeer resource // NBRoutingPeerCustomValidator struct is responsible for validating the NBRoutingPeer resource
// when it is created, updated, or deleted. // when it is created, updated, or deleted.
type NBRoutingPeerCustomValidator struct { type NBRoutingPeerCustomValidator struct {
netbird *netbird.Client client client.Client
client client.Client
} }
var _ webhook.CustomValidator = &NBRoutingPeerCustomValidator{} var _ webhook.CustomValidator = &NBRoutingPeerCustomValidator{}
@@ -63,11 +61,13 @@ func (v *NBRoutingPeerCustomValidator) ValidateDelete(ctx context.Context, obj r
return nil, err return nil, err
} }
resourceValidator := &NBResourceCustomValidator{client: v.client}
for _, r := range nbResources.Items { for _, r := range nbResources.Items {
if r.Spec.NetworkID == *nbroutingpeer.Status.NetworkID { if r.Spec.NetworkID == *nbroutingpeer.Status.NetworkID {
err = v.client.Delete(ctx, &r, client.DryRunAll) _, err = resourceValidator.ValidateDelete(ctx, &r)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s/%s: %w", r.Namespace, r.Name, err) return nil, err
} }
} }
} }
+217 -31
View File
@@ -3,9 +3,14 @@ package v1
import ( import (
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1" netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1"
// TODO (user): Add any additional imports if needed "github.com/netbirdio/kubernetes-operator/internal/util"
) )
var _ = Describe("NBRoutingPeer Webhook", func() { var _ = Describe("NBRoutingPeer Webhook", func() {
@@ -16,41 +21,222 @@ var _ = Describe("NBRoutingPeer Webhook", func() {
) )
BeforeEach(func() { BeforeEach(func() {
Skip("Not implemented yet")
obj = &netbirdiov1.NBRoutingPeer{} obj = &netbirdiov1.NBRoutingPeer{}
oldObj = &netbirdiov1.NBRoutingPeer{} oldObj = &netbirdiov1.NBRoutingPeer{}
validator = NBRoutingPeerCustomValidator{} validator = NBRoutingPeerCustomValidator{
Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") client: k8sClient,
Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") }
Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
// TODO (user): Add any setup logic common to all tests
})
AfterEach(func() {
// TODO (user): Add any teardown logic common to all tests
}) })
Context("When creating or updating NBRoutingPeer under Validating Webhook", func() { Context("When creating or updating NBRoutingPeer under Validating Webhook", func() {
// TODO (user): Add logic for validating webhooks It("should allow creation", func() {
// Example: Expect(validator.ValidateCreate(ctx, obj)).Error().NotTo(HaveOccurred())
// It("Should deny creation if a required field is missing", func() { })
// By("simulating an invalid creation scenario") It("should allow update", func() {
// obj.SomeRequiredField = "" Expect(validator.ValidateUpdate(ctx, oldObj, obj)).Error().NotTo(HaveOccurred())
// Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred()) })
// }) When("No NBResources Exist", func() {
// It("should allow deletion", func() {
// It("Should admit creation if all required fields are present", func() { Expect(validator.ValidateDelete(ctx, obj)).Error().NotTo(HaveOccurred())
// By("simulating an invalid creation scenario") })
// obj.SomeRequiredField = "valid_value" })
// Expect(validator.ValidateCreate(ctx, obj)).To(BeNil()) When("Deleteable NBResources Exist", func() {
// }) BeforeEach(func() {
// nbResource := &netbirdiov1.NBResource{
// It("Should validate updates correctly", func() { ObjectMeta: v1.ObjectMeta{
// By("simulating a valid update scenario") Name: "isexist",
// oldObj.SomeRequiredField = "updated_value" Namespace: "default",
// obj.SomeRequiredField = "updated_value" },
// Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil()) Spec: netbirdiov1.NBResourceSpec{
// }) Name: "test1",
NetworkID: "test2",
Address: "test3",
Groups: []string{"test"},
},
}
Expect(k8sClient.Create(ctx, nbResource)).To(Succeed())
obj = &netbirdiov1.NBRoutingPeer{
Status: netbirdiov1.NBRoutingPeerStatus{
NetworkID: util.Ptr("test2"),
},
}
})
AfterEach(func() {
nbResource := &netbirdiov1.NBResource{}
err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "isexist"}, nbResource)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
if len(nbResource.Finalizers) > 0 {
nbResource.Finalizers = nil
Expect(k8sClient.Update(ctx, nbResource)).To(Succeed())
}
err = k8sClient.Delete(ctx, nbResource)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
}
}
})
It("should allow deletion", func() {
Expect(validator.ValidateDelete(ctx, obj)).Error().NotTo(HaveOccurred())
})
})
When("Exposed Services for Network Exist", func() {
BeforeEach(func() {
nbResource := &netbirdiov1.NBResource{
ObjectMeta: v1.ObjectMeta{
Name: "maw",
Namespace: "default",
},
Spec: netbirdiov1.NBResourceSpec{
Name: "test1",
NetworkID: "test2",
Address: "test3",
Groups: []string{"test"},
},
}
Expect(k8sClient.Create(ctx, nbResource)).To(Succeed())
svc := &corev1.Service{
ObjectMeta: v1.ObjectMeta{
Name: "maw",
Namespace: "default",
Annotations: map[string]string{
"netbird.io/expose": "true",
},
},
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Protocol: corev1.ProtocolTCP,
Port: 80,
TargetPort: intstr.FromInt32(80),
},
},
},
}
Expect(k8sClient.Create(ctx, svc)).To(Succeed())
obj = &netbirdiov1.NBRoutingPeer{
Status: netbirdiov1.NBRoutingPeerStatus{
NetworkID: util.Ptr("test2"),
},
}
})
AfterEach(func() {
nbResource := &netbirdiov1.NBResource{}
err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "maw"}, nbResource)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
if len(nbResource.Finalizers) > 0 {
nbResource.Finalizers = nil
Expect(k8sClient.Update(ctx, nbResource)).To(Succeed())
}
err = k8sClient.Delete(ctx, nbResource)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
}
}
svc := &corev1.Service{}
err = k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "maw"}, svc)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
if len(svc.Finalizers) > 0 {
svc.Finalizers = nil
Expect(k8sClient.Update(ctx, svc)).To(Succeed())
}
err = k8sClient.Delete(ctx, svc)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
}
}
})
It("should deny deletion", func() {
Expect(validator.ValidateDelete(ctx, obj)).Error().To(HaveOccurred())
})
})
When("Exposed NBResources do not belong to network", func() {
BeforeEach(func() {
nbResource := &netbirdiov1.NBResource{
ObjectMeta: v1.ObjectMeta{
Name: "maw",
Namespace: "default",
},
Spec: netbirdiov1.NBResourceSpec{
Name: "test1",
NetworkID: "test5",
Address: "test3",
Groups: []string{"test"},
},
}
Expect(k8sClient.Create(ctx, nbResource)).To(Succeed())
svc := &corev1.Service{
ObjectMeta: v1.ObjectMeta{
Name: "maw",
Namespace: "default",
Annotations: map[string]string{
"netbird.io/expose": "true",
},
},
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Protocol: corev1.ProtocolTCP,
Port: 80,
TargetPort: intstr.FromInt32(80),
},
},
},
}
Expect(k8sClient.Create(ctx, svc)).To(Succeed())
obj = &netbirdiov1.NBRoutingPeer{
Status: netbirdiov1.NBRoutingPeerStatus{
NetworkID: util.Ptr("test2"),
},
}
})
AfterEach(func() {
nbResource := &netbirdiov1.NBResource{}
err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "maw"}, nbResource)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
if len(nbResource.Finalizers) > 0 {
nbResource.Finalizers = nil
Expect(k8sClient.Update(ctx, nbResource)).To(Succeed())
}
err = k8sClient.Delete(ctx, nbResource)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
}
}
svc := &corev1.Service{}
err = k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "maw"}, svc)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
if len(svc.Finalizers) > 0 {
svc.Finalizers = nil
Expect(k8sClient.Update(ctx, svc)).To(Succeed())
}
err = k8sClient.Delete(ctx, svc)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
}
}
})
It("should allow deletion", func() {
Expect(validator.ValidateDelete(ctx, obj)).Error().NotTo(HaveOccurred())
})
})
}) })
}) })
+91 -5
View File
@@ -6,6 +6,7 @@ import (
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
@@ -30,11 +31,6 @@ var _ = Describe("NBSetupKey Webhook", func() {
validator = NBSetupKeyCustomValidator{ validator = NBSetupKeyCustomValidator{
client: k8sClient, client: k8sClient,
} }
Expect(validator).NotTo(BeNil(), "Expected validator to be initialized")
Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
})
AfterEach(func() {
}) })
Context("When creating or updating NBSetupKey under Validating Webhook", func() { Context("When creating or updating NBSetupKey under Validating Webhook", func() {
@@ -119,6 +115,96 @@ var _ = Describe("NBSetupKey Webhook", func() {
}) })
}) })
}) })
Context("Delete", func() {
When("No pods exist with annotation", func() {
BeforeEach(func() {
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "notannotated",
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "test", Image: "test"},
},
},
}
Expect(k8sClient.Create(ctx, pod)).To(Succeed())
})
AfterEach(func() {
pod := &corev1.Pod{}
err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "notannotated"}, pod)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
if len(pod.Finalizers) > 0 {
pod.Finalizers = nil
Expect(k8sClient.Update(ctx, pod)).To(Succeed())
}
err = k8sClient.Delete(ctx, pod)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
}
}
})
It("should allow delete", func() {
obj = &netbirdiov1.NBSetupKey{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
}
Expect(validator.ValidateDelete(ctx, obj)).Error().NotTo(HaveOccurred())
})
})
When("Pods exist with annotation", func() {
BeforeEach(func() {
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "annotated",
Annotations: map[string]string{
setupKeyAnnotation: "test",
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "test", Image: "test"},
},
},
}
Expect(k8sClient.Create(ctx, pod)).To(Succeed())
})
AfterEach(func() {
pod := &corev1.Pod{}
err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "annotated"}, pod)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
if len(pod.Finalizers) > 0 {
pod.Finalizers = nil
Expect(k8sClient.Update(ctx, pod)).To(Succeed())
}
err = k8sClient.Delete(ctx, pod)
if !errors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
}
}
})
It("should deny delete", func() {
obj = &netbirdiov1.NBSetupKey{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
}
Expect(validator.ValidateDelete(ctx, obj)).Error().To(HaveOccurred())
})
})
})
}) })
}) })
+3 -3
View File
@@ -124,13 +124,13 @@ var _ = BeforeSuite(func() {
err = SetupNBSetupKeyWebhookWithManager(mgr) err = SetupNBSetupKeyWebhookWithManager(mgr)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
err = SetupNBResourceWebhookWithManager(mgr, "", "") err = SetupNBResourceWebhookWithManager(mgr)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
err = SetupNBRoutingPeerWebhookWithManager(mgr, "", "") err = SetupNBRoutingPeerWebhookWithManager(mgr)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
err = SetupNBGroupWebhookWithManager(mgr, "", "") err = SetupNBGroupWebhookWithManager(mgr)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
// +kubebuilder:scaffold:webhook // +kubebuilder:scaffold:webhook