diff --git a/PROJECT b/PROJECT index 8b05654..166eb1c 100644 --- a/PROJECT +++ b/PROJECT @@ -83,4 +83,12 @@ resources: kind: SetupKey path: github.com/netbirdio/kubernetes-operator/api/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: netbird.io + kind: Group + path: github.com/netbirdio/kubernetes-operator/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/api/v1alpha1/group_types.go b/api/v1alpha1/group_types.go new file mode 100644 index 0000000..62da5b7 --- /dev/null +++ b/api/v1alpha1/group_types.go @@ -0,0 +1,58 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// GroupSpec defines the desired state of Group +type GroupSpec struct { + // name of the group. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` +} + +// GroupStatus defines the observed state of Group. +type GroupStatus struct { + // +optional + GroupID *string `json:"groupID,omitempty"` + + // The status of each condition is one of True, False, or Unknown. + // +listType=map + // +listMapKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource + +// Group is the Schema for the groups API +type Group struct { + metav1.TypeMeta `json:",inline"` + + // metadata is a standard object metadata + // +optional + metav1.ObjectMeta `json:"metadata,omitzero"` + + // spec defines the desired state of Group + // +required + Spec GroupSpec `json:"spec"` + + // status defines the observed state of Group + // +optional + Status GroupStatus `json:"status,omitzero"` +} + +// +kubebuilder:object:root=true + +// GroupList contains a list of Group +type GroupList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitzero"` + Items []Group `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Group{}, &GroupList{}) +} diff --git a/api/v1alpha1/reference_type.go b/api/v1alpha1/reference_type.go new file mode 100644 index 0000000..670170d --- /dev/null +++ b/api/v1alpha1/reference_type.go @@ -0,0 +1,14 @@ +package v1alpha1 + +import corev1 "k8s.io/api/core/v1" + +// +kubebuilder:validation:XValidation:rule="(has(self.id) && !has(self.localRef)) || (!has(self.id) && has(self.localRef))",message="exactly one of id or localRef must be set" +type ResourceReference struct { + // id of the resource in the Netbird API. + // +optional + ID *string `json:"id,omitempty"` + + // local reference to the object in the same namespace. + // +optional + LocalRef *corev1.LocalObjectReference `json:"localRef,omitempty"` +} diff --git a/api/v1alpha1/setupkey_types.go b/api/v1alpha1/setupkey_types.go index b43f446..94ff4b6 100644 --- a/api/v1alpha1/setupkey_types.go +++ b/api/v1alpha1/setupkey_types.go @@ -14,6 +14,10 @@ type SetupKeySpec struct { // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="duration is immutable" // +optional Duration *metav1.Duration `json:"duration,omitempty"` + + // Groups that will be automatically assigned to resources using setup key. + // +optional + AutoGroups []ResourceReference `json:"autoGroups,omitempty"` } // SetupKeyStatus defines the observed state of SetupKey. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index fbeb852..b416ac4 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -5,10 +5,137 @@ package v1alpha1 import ( + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Group) DeepCopyInto(out *Group) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Group. +func (in *Group) DeepCopy() *Group { + if in == nil { + return nil + } + out := new(Group) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Group) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GroupList) DeepCopyInto(out *GroupList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Group, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroupList. +func (in *GroupList) DeepCopy() *GroupList { + if in == nil { + return nil + } + out := new(GroupList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GroupList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GroupSpec) DeepCopyInto(out *GroupSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroupSpec. +func (in *GroupSpec) DeepCopy() *GroupSpec { + if in == nil { + return nil + } + out := new(GroupSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GroupStatus) DeepCopyInto(out *GroupStatus) { + *out = *in + if in.GroupID != nil { + in, out := &in.GroupID, &out.GroupID + *out = new(string) + **out = **in + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroupStatus. +func (in *GroupStatus) DeepCopy() *GroupStatus { + if in == nil { + return nil + } + out := new(GroupStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceReference) DeepCopyInto(out *ResourceReference) { + *out = *in + if in.ID != nil { + in, out := &in.ID, &out.ID + *out = new(string) + **out = **in + } + if in.LocalRef != nil { + in, out := &in.LocalRef, &out.LocalRef + *out = new(corev1.LocalObjectReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceReference. +func (in *ResourceReference) DeepCopy() *ResourceReference { + if in == nil { + return nil + } + out := new(ResourceReference) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SetupKey) DeepCopyInto(out *SetupKey) { *out = *in @@ -76,6 +203,13 @@ func (in *SetupKeySpec) DeepCopyInto(out *SetupKeySpec) { *out = new(v1.Duration) **out = **in } + if in.AutoGroups != nil { + in, out := &in.AutoGroups, &out.AutoGroups + *out = make([]ResourceReference, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SetupKeySpec. diff --git a/cmd/main.go b/cmd/main.go index 931f3dc..ce22865 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -290,6 +290,14 @@ func main() { setupLog.Error(err, "Failed to create controller", "controller", "SetupKey") os.Exit(1) } + if err := (&controller.GroupReconciler{ + Client: mgr.GetClient(), + Netbird: netbird, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "Failed to create controller", "controller", "Group") + os.Exit(1) + } + if gatewayAPIEnabled { if err = (&controller.GatewayClassReconciler{ Client: mgr.GetClient(), diff --git a/examples/refactor/group.yaml b/examples/refactor/group.yaml new file mode 100644 index 0000000..db9b9c6 --- /dev/null +++ b/examples/refactor/group.yaml @@ -0,0 +1,7 @@ +apiVersion: netbird.io/v1alpha1 +kind: Group +metadata: + name: test + namespace: default +spec: + name: test diff --git a/helm/kubernetes-operator/crds/netbird.io_groups.yaml b/helm/kubernetes-operator/crds/netbird.io_groups.yaml new file mode 100644 index 0000000..823ea25 --- /dev/null +++ b/helm/kubernetes-operator/crds/netbird.io_groups.yaml @@ -0,0 +1,122 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.1 + name: groups.netbird.io +spec: + group: netbird.io + names: + kind: Group + listKind: GroupList + plural: groups + singular: group + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Group is the Schema for the groups API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: spec defines the desired state of Group + properties: + name: + description: name of the group. + minLength: 1 + type: string + required: + - name + type: object + status: + description: status defines the observed state of Group + properties: + conditions: + description: The status of each condition is one of True, False, or + Unknown. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + groupID: + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/helm/kubernetes-operator/crds/netbird.io_setupkeys.yaml b/helm/kubernetes-operator/crds/netbird.io_setupkeys.yaml index 5d4a42b..83ce536 100644 --- a/helm/kubernetes-operator/crds/netbird.io_setupkeys.yaml +++ b/helm/kubernetes-operator/crds/netbird.io_setupkeys.yaml @@ -39,6 +39,34 @@ spec: spec: description: spec defines the desired state of SetupKey properties: + autoGroups: + description: Groups that will be automatically assigned to resources + using setup key. + items: + properties: + id: + description: id of the resource in the Netbird API. + type: string + localRef: + description: local reference to the object in the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: exactly one of id or localRef must be set + rule: (has(self.id) && !has(self.localRef)) || (!has(self.id) + && has(self.localRef)) + type: array duration: description: Duration sets how long the setup key is valid for. type: string diff --git a/internal/controller/group_controller.go b/internal/controller/group_controller.go new file mode 100644 index 0000000..070cbc8 --- /dev/null +++ b/internal/controller/group_controller.go @@ -0,0 +1,103 @@ +package controller + +import ( + "context" + + netbird "github.com/netbirdio/netbird/shared/management/client/rest" + "github.com/netbirdio/netbird/shared/management/http/api" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + nbv1alpha1 "github.com/netbirdio/kubernetes-operator/api/v1alpha1" + nbv1alpha1ac "github.com/netbirdio/kubernetes-operator/pkg/applyconfigurations/api/v1alpha1" +) + +const ( + GroupFinalizer = "netbird.io/group" +) + +// GroupReconciler reconciles a Group object +type GroupReconciler struct { + client.Client + + Netbird *netbird.Client +} + +// +kubebuilder:rbac:groups=netbird.io,resources=groups,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=netbird.io,resources=groups/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=netbird.io,resources=groups/finalizers,verbs=update +func (r *GroupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + group := nbv1alpha1.Group{} + err := r.Get(ctx, req.NamespacedName, &group) + if err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + if !group.DeletionTimestamp.IsZero() { + return r.reconcileDelete(ctx, group) + } + + groupAC := nbv1alpha1ac.Group(req.Name, req.Namespace).WithFinalizers(SetupKeyFinalizer) + err = r.Client.Apply(ctx, groupAC) + if err != nil { + return ctrl.Result{}, err + } + + groupID, err := func() (string, error) { + if group.Status.GroupID != nil { + groupReq := api.GroupRequest{ + Name: group.Spec.Name, + } + resp, err := r.Netbird.Groups.Update(ctx, *group.Status.GroupID, groupReq) + if err != nil && !netbird.IsNotFound(err) { + return "", err + } + if err == nil { + return resp.Id, nil + } + } + + groupReq := api.GroupRequest{ + Name: group.Spec.Name, + } + resp, err := r.Netbird.Groups.Create(ctx, groupReq) + if err != nil { + return "", err + } + return resp.Id, nil + }() + if err != nil { + return ctrl.Result{}, err + } + + groupAC = nbv1alpha1ac.Group(req.Name, req.Namespace).WithStatus(nbv1alpha1ac.GroupStatus().WithGroupID(groupID)) + err = r.Client.Status().Apply(ctx, groupAC) + if err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +func (r *GroupReconciler) reconcileDelete(ctx context.Context, group nbv1alpha1.Group) (ctrl.Result, error) { + if group.Status.GroupID != nil { + err := r.Netbird.Groups.Delete(ctx, *group.Status.GroupID) + if err != nil && !netbird.IsNotFound(err) { + return ctrl.Result{}, err + } + } + + groupAC := nbv1alpha1ac.Group(group.Name, group.Namespace).WithFinalizers() + err := r.Client.Apply(ctx, groupAC) + if err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *GroupReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&nbv1alpha1.Group{}). + Complete(r) +} diff --git a/internal/controller/group_controller_test.go b/internal/controller/group_controller_test.go new file mode 100644 index 0000000..ee5ff75 --- /dev/null +++ b/internal/controller/group_controller_test.go @@ -0,0 +1,139 @@ +package controller + +import ( + "context" + "encoding/json" + "fmt" + "io" + "math/rand" + "net/http" + "net/http/httptest" + + netbird "github.com/netbirdio/netbird/shared/management/client/rest" + "github.com/netbirdio/netbird/shared/management/http/api" + "github.com/netbirdio/netbird/shared/management/http/util" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + kerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + nbv1alpha1 "github.com/netbirdio/kubernetes-operator/api/v1alpha1" +) + +var _ = Describe("Group Controller", func() { + Context("When reconciling a resource", func() { + ctx := context.Background() + + r := rand.New(rand.NewSource(GinkgoRandomSeed())) + groupStore := map[string]*api.Group{} + mux := &http.ServeMux{} + mux.Handle("POST /api/groups", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + b, err := io.ReadAll(req.Body) + Expect(err).NotTo(HaveOccurred()) + groupReq := api.GroupRequest{} + err = json.Unmarshal(b, &groupReq) + Expect(err).NotTo(HaveOccurred()) + + groupResp := &api.Group{ + Id: fmt.Sprintf("id-%d", r.Int63()), + Name: groupReq.Name, + } + groupStore[groupResp.Id] = groupResp + b, err = json.Marshal(groupResp) + Expect(err).NotTo(HaveOccurred()) + _, err = rw.Write(b) + Expect(err).NotTo(HaveOccurred()) + })) + mux.Handle("PUT /api/groups/{id}", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + id := req.PathValue("id") + groupResp, ok := groupStore[id] + if !ok { + util.WriteErrorResponse("Not Found", http.StatusNotFound, rw) + return + } + + b, err := io.ReadAll(req.Body) + Expect(err).NotTo(HaveOccurred()) + groupReq := api.GroupRequest{} + err = json.Unmarshal(b, &groupReq) + Expect(err).NotTo(HaveOccurred()) + + groupResp.Name = groupReq.Name + + b, err = json.Marshal(groupResp) + Expect(err).NotTo(HaveOccurred()) + _, err = rw.Write(b) + Expect(err).NotTo(HaveOccurred()) + })) + mux.Handle("DELETE /api/groups/{id}", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + id := req.PathValue("id") + _, ok := groupStore[id] + if !ok { + util.WriteErrorResponse("Not Found", http.StatusNotFound, rw) + return + } + delete(groupStore, id) + })) + server := httptest.NewServer(mux) + nbClient := netbird.New(server.URL, "ABC") + + var controllerReconciler *GroupReconciler + nn := client.ObjectKey{ + Name: "test-resource", + Namespace: "default", + } + + BeforeEach(func() { + controllerReconciler = &GroupReconciler{ + Client: k8sClient, + Netbird: nbClient, + } + }) + + AfterEach(func() { + groupStore = map[string]*api.Group{} + + group := &nbv1alpha1.Group{} + err := k8sClient.Get(ctx, nn, group) + if kerrors.IsNotFound(err) { + return + } + Expect(err).ToNot(HaveOccurred()) + Expect(k8sClient.Delete(ctx, group)).To(Succeed()) + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: nn}) + Expect(err).ToNot(HaveOccurred()) + }) + + It("ensures a Netbird group exists on reconcile", func() { + group := &nbv1alpha1.Group{ + ObjectMeta: metav1.ObjectMeta{ + Name: nn.Name, + Namespace: nn.Namespace, + }, + Spec: nbv1alpha1.GroupSpec{ + Name: "foobar", + }, + } + Expect(k8sClient.Create(ctx, group)).To(Succeed()) + + By("creating a group on initial creation") + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: nn}) + Expect(err).NotTo(HaveOccurred()) + err = k8sClient.Get(ctx, nn, group) + Expect(err).NotTo(HaveOccurred()) + Expect(*group.Status.GroupID).NotTo(BeEmpty()) + + By("crerating a new group when deleted from API") + delete(groupStore, *group.Status.GroupID) + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: nn}) + Expect(err).NotTo(HaveOccurred()) + newGroup := &nbv1alpha1.Group{} + err = k8sClient.Get(ctx, nn, newGroup) + Expect(err).NotTo(HaveOccurred()) + Expect(*newGroup.Status.GroupID).NotTo(BeEmpty()) + Expect(*newGroup.Status.GroupID).NotTo(Equal(*group.Status.GroupID)) + }) + }) +}) diff --git a/internal/controller/setupkey_controller.go b/internal/controller/setupkey_controller.go index d6bbf01..587b82b 100644 --- a/internal/controller/setupkey_controller.go +++ b/internal/controller/setupkey_controller.go @@ -3,12 +3,14 @@ package controller import ( "context" "errors" + "fmt" "time" netbird "github.com/netbirdio/netbird/shared/management/client/rest" "github.com/netbirdio/netbird/shared/management/http/api" corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" corev1ac "k8s.io/client-go/applyconfigurations/core/v1" "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" @@ -44,6 +46,34 @@ func (r *SetupKeyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c return r.reconcileDelete(ctx, setupKey) } + // Get ids for auto groups. + autoGroupIDs := []string{} + for _, ref := range setupKey.Spec.AutoGroups { + switch { + case ref.ID != nil: + _, err := r.Netbird.Groups.Get(ctx, *ref.ID) + if err != nil { + return ctrl.Result{}, err + } + autoGroupIDs = append(autoGroupIDs, *ref.ID) + case ref.LocalRef != nil: + group := nbv1alpha1.Group{ + ObjectMeta: metav1.ObjectMeta{ + Name: ref.LocalRef.Name, + Namespace: setupKey.Namespace, + }, + } + err = r.Client.Get(ctx, client.ObjectKeyFromObject(&group), &group) + if err != nil { + return ctrl.Result{}, err + } + if group.Status.GroupID == nil { + return ctrl.Result{}, fmt.Errorf("group %s in auto groups list is not ready", group.Name) + } + autoGroupIDs = append(autoGroupIDs, *group.Status.GroupID) + } + } + // Set finalizer on the setup key. setupKeyAC := nbv1alpha1ac.SetupKey(req.Name, req.Namespace).WithFinalizers(SetupKeyFinalizer) err = r.Client.Apply(ctx, setupKeyAC) @@ -89,7 +119,7 @@ func (r *SetupKeyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c // Auto groups have not been changed. setupKeyReq := api.PutApiSetupKeysKeyIdJSONRequestBody{ - AutoGroups: []string{}, + AutoGroups: autoGroupIDs, } _, err = r.Netbird.SetupKeys.Update(ctx, *setupKey.Status.SetupKeyID, setupKeyReq) if err != nil { @@ -113,7 +143,7 @@ func (r *SetupKeyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c } setupKeyReq := api.PostApiSetupKeysJSONRequestBody{ AllowExtraDnsLabels: ptr.To(false), - AutoGroups: []string{}, + AutoGroups: autoGroupIDs, Ephemeral: ptr.To(setupKey.Spec.Ephemeral), ExpiresIn: expiresIn, Name: req.Name, @@ -160,17 +190,15 @@ func (r *SetupKeyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c } func (r *SetupKeyReconciler) reconcileDelete(ctx context.Context, setupKey nbv1alpha1.SetupKey) (ctrl.Result, error) { - if setupKey.Status.SetupKeyID == nil { - return ctrl.Result{}, nil - } - - err := r.Netbird.SetupKeys.Delete(ctx, *setupKey.Status.SetupKeyID) - if err != nil && !netbird.IsNotFound(err) { - return ctrl.Result{}, err + if setupKey.Status.SetupKeyID != nil { + err := r.Netbird.SetupKeys.Delete(ctx, *setupKey.Status.SetupKeyID) + if err != nil && !netbird.IsNotFound(err) { + return ctrl.Result{}, err + } } setupKeyAC := nbv1alpha1ac.SetupKey(setupKey.Name, setupKey.Namespace).WithFinalizers() - err = r.Client.Apply(ctx, setupKeyAC) + err := r.Client.Apply(ctx, setupKeyAC) if err != nil { return ctrl.Result{}, err } diff --git a/internal/controller/setupkey_controller_test.go b/internal/controller/setupkey_controller_test.go index 748c746..3bd4edc 100644 --- a/internal/controller/setupkey_controller_test.go +++ b/internal/controller/setupkey_controller_test.go @@ -27,7 +27,6 @@ var _ = Describe("SetupKey Controller", func() { ctx := context.Background() r := rand.New(rand.NewSource(GinkgoRandomSeed())) - setupKeyStore := map[string]*api.SetupKey{} mux := &http.ServeMux{} mux.HandleFunc("/api/setup-keys", func(rw http.ResponseWriter, req *http.Request) { diff --git a/pkg/applyconfigurations/api/v1alpha1/group.go b/pkg/applyconfigurations/api/v1alpha1/group.go new file mode 100644 index 0000000..a812d6f --- /dev/null +++ b/pkg/applyconfigurations/api/v1alpha1/group.go @@ -0,0 +1,232 @@ +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// GroupApplyConfiguration represents a declarative configuration of the Group type for use +// with apply. +// +// Group is the Schema for the groups API +type GroupApplyConfiguration struct { + v1.TypeMetaApplyConfiguration `json:",inline"` + // metadata is a standard object metadata + *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + // spec defines the desired state of Group + Spec *GroupSpecApplyConfiguration `json:"spec,omitempty"` + // status defines the observed state of Group + Status *GroupStatusApplyConfiguration `json:"status,omitempty"` +} + +// Group constructs a declarative configuration of the Group type for use with +// apply. +func Group(name, namespace string) *GroupApplyConfiguration { + b := &GroupApplyConfiguration{} + b.WithName(name) + b.WithNamespace(namespace) + b.WithKind("Group") + b.WithAPIVersion("netbird.io/v1alpha1") + return b +} + +func (b GroupApplyConfiguration) IsApplyConfiguration() {} + +// WithKind sets the Kind field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Kind field is set to the value of the last call. +func (b *GroupApplyConfiguration) WithKind(value string) *GroupApplyConfiguration { + b.TypeMetaApplyConfiguration.Kind = &value + return b +} + +// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the APIVersion field is set to the value of the last call. +func (b *GroupApplyConfiguration) WithAPIVersion(value string) *GroupApplyConfiguration { + b.TypeMetaApplyConfiguration.APIVersion = &value + return b +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *GroupApplyConfiguration) WithName(value string) *GroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Name = &value + return b +} + +// WithGenerateName sets the GenerateName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GenerateName field is set to the value of the last call. +func (b *GroupApplyConfiguration) WithGenerateName(value string) *GroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.GenerateName = &value + return b +} + +// WithNamespace sets the Namespace field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Namespace field is set to the value of the last call. +func (b *GroupApplyConfiguration) WithNamespace(value string) *GroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Namespace = &value + return b +} + +// WithUID sets the UID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UID field is set to the value of the last call. +func (b *GroupApplyConfiguration) WithUID(value types.UID) *GroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.UID = &value + return b +} + +// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ResourceVersion field is set to the value of the last call. +func (b *GroupApplyConfiguration) WithResourceVersion(value string) *GroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.ResourceVersion = &value + return b +} + +// WithGeneration sets the Generation field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Generation field is set to the value of the last call. +func (b *GroupApplyConfiguration) WithGeneration(value int64) *GroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Generation = &value + return b +} + +// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CreationTimestamp field is set to the value of the last call. +func (b *GroupApplyConfiguration) WithCreationTimestamp(value metav1.Time) *GroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.CreationTimestamp = &value + return b +} + +// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionTimestamp field is set to the value of the last call. +func (b *GroupApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *GroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value + return b +} + +// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. +func (b *GroupApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *GroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *GroupApplyConfiguration) WithLabels(entries map[string]string) *GroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *GroupApplyConfiguration) WithAnnotations(entries map[string]string) *GroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Annotations[k] = v + } + return b +} + +// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the OwnerReferences field. +func (b *GroupApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *GroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + if values[i] == nil { + panic("nil value passed to WithOwnerReferences") + } + b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i]) + } + return b +} + +// WithFinalizers adds the given value to the Finalizers field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Finalizers field. +func (b *GroupApplyConfiguration) WithFinalizers(values ...string) *GroupApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) + } + return b +} + +func (b *GroupApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { + if b.ObjectMetaApplyConfiguration == nil { + b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} + } +} + +// WithSpec sets the Spec field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Spec field is set to the value of the last call. +func (b *GroupApplyConfiguration) WithSpec(value *GroupSpecApplyConfiguration) *GroupApplyConfiguration { + b.Spec = value + return b +} + +// WithStatus sets the Status field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Status field is set to the value of the last call. +func (b *GroupApplyConfiguration) WithStatus(value *GroupStatusApplyConfiguration) *GroupApplyConfiguration { + b.Status = value + return b +} + +// GetKind retrieves the value of the Kind field in the declarative configuration. +func (b *GroupApplyConfiguration) GetKind() *string { + return b.TypeMetaApplyConfiguration.Kind +} + +// GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration. +func (b *GroupApplyConfiguration) GetAPIVersion() *string { + return b.TypeMetaApplyConfiguration.APIVersion +} + +// GetName retrieves the value of the Name field in the declarative configuration. +func (b *GroupApplyConfiguration) GetName() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Name +} + +// GetNamespace retrieves the value of the Namespace field in the declarative configuration. +func (b *GroupApplyConfiguration) GetNamespace() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Namespace +} diff --git a/pkg/applyconfigurations/api/v1alpha1/groupspec.go b/pkg/applyconfigurations/api/v1alpha1/groupspec.go new file mode 100644 index 0000000..7ccf93e --- /dev/null +++ b/pkg/applyconfigurations/api/v1alpha1/groupspec.go @@ -0,0 +1,26 @@ +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +// GroupSpecApplyConfiguration represents a declarative configuration of the GroupSpec type for use +// with apply. +// +// GroupSpec defines the desired state of Group +type GroupSpecApplyConfiguration struct { + // name of the group. + Name *string `json:"name,omitempty"` +} + +// GroupSpecApplyConfiguration constructs a declarative configuration of the GroupSpec type for use with +// apply. +func GroupSpec() *GroupSpecApplyConfiguration { + return &GroupSpecApplyConfiguration{} +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *GroupSpecApplyConfiguration) WithName(value string) *GroupSpecApplyConfiguration { + b.Name = &value + return b +} diff --git a/pkg/applyconfigurations/api/v1alpha1/groupstatus.go b/pkg/applyconfigurations/api/v1alpha1/groupstatus.go new file mode 100644 index 0000000..4a1d54c --- /dev/null +++ b/pkg/applyconfigurations/api/v1alpha1/groupstatus.go @@ -0,0 +1,44 @@ +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// GroupStatusApplyConfiguration represents a declarative configuration of the GroupStatus type for use +// with apply. +// +// GroupStatus defines the observed state of Group. +type GroupStatusApplyConfiguration struct { + GroupID *string `json:"groupID,omitempty"` + // The status of each condition is one of True, False, or Unknown. + Conditions []v1.ConditionApplyConfiguration `json:"conditions,omitempty"` +} + +// GroupStatusApplyConfiguration constructs a declarative configuration of the GroupStatus type for use with +// apply. +func GroupStatus() *GroupStatusApplyConfiguration { + return &GroupStatusApplyConfiguration{} +} + +// WithGroupID sets the GroupID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GroupID field is set to the value of the last call. +func (b *GroupStatusApplyConfiguration) WithGroupID(value string) *GroupStatusApplyConfiguration { + b.GroupID = &value + return b +} + +// WithConditions adds the given value to the Conditions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Conditions field. +func (b *GroupStatusApplyConfiguration) WithConditions(values ...*v1.ConditionApplyConfiguration) *GroupStatusApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithConditions") + } + b.Conditions = append(b.Conditions, *values[i]) + } + return b +} diff --git a/pkg/applyconfigurations/api/v1alpha1/resourcereference.go b/pkg/applyconfigurations/api/v1alpha1/resourcereference.go new file mode 100644 index 0000000..3ac7078 --- /dev/null +++ b/pkg/applyconfigurations/api/v1alpha1/resourcereference.go @@ -0,0 +1,38 @@ +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1 "k8s.io/api/core/v1" +) + +// ResourceReferenceApplyConfiguration represents a declarative configuration of the ResourceReference type for use +// with apply. +type ResourceReferenceApplyConfiguration struct { + // id of the resource in the Netbird API. + ID *string `json:"id,omitempty"` + // local reference to the object in the same namespace. + LocalRef *v1.LocalObjectReference `json:"localRef,omitempty"` +} + +// ResourceReferenceApplyConfiguration constructs a declarative configuration of the ResourceReference type for use with +// apply. +func ResourceReference() *ResourceReferenceApplyConfiguration { + return &ResourceReferenceApplyConfiguration{} +} + +// WithID sets the ID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ID field is set to the value of the last call. +func (b *ResourceReferenceApplyConfiguration) WithID(value string) *ResourceReferenceApplyConfiguration { + b.ID = &value + return b +} + +// WithLocalRef sets the LocalRef field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the LocalRef field is set to the value of the last call. +func (b *ResourceReferenceApplyConfiguration) WithLocalRef(value v1.LocalObjectReference) *ResourceReferenceApplyConfiguration { + b.LocalRef = &value + return b +} diff --git a/pkg/applyconfigurations/api/v1alpha1/setupkeyspec.go b/pkg/applyconfigurations/api/v1alpha1/setupkeyspec.go index 938c2db..50ef2e8 100644 --- a/pkg/applyconfigurations/api/v1alpha1/setupkeyspec.go +++ b/pkg/applyconfigurations/api/v1alpha1/setupkeyspec.go @@ -15,6 +15,8 @@ type SetupKeySpecApplyConfiguration struct { Ephemeral *bool `json:"ephemeral,omitempty"` // Duration sets how long the setup key is valid for. Duration *v1.Duration `json:"duration,omitempty"` + // Groups that will be automatically assigned to resources using setup key. + AutoGroups []ResourceReferenceApplyConfiguration `json:"autoGroups,omitempty"` } // SetupKeySpecApplyConfiguration constructs a declarative configuration of the SetupKeySpec type for use with @@ -38,3 +40,16 @@ func (b *SetupKeySpecApplyConfiguration) WithDuration(value v1.Duration) *SetupK b.Duration = &value return b } + +// WithAutoGroups adds the given value to the AutoGroups field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the AutoGroups field. +func (b *SetupKeySpecApplyConfiguration) WithAutoGroups(values ...*ResourceReferenceApplyConfiguration) *SetupKeySpecApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithAutoGroups") + } + b.AutoGroups = append(b.AutoGroups, *values[i]) + } + return b +} diff --git a/pkg/applyconfigurations/utils.go b/pkg/applyconfigurations/utils.go index 2115ed4..f596e4e 100644 --- a/pkg/applyconfigurations/utils.go +++ b/pkg/applyconfigurations/utils.go @@ -16,6 +16,14 @@ import ( func ForKind(kind schema.GroupVersionKind) interface{} { switch kind { // Group=netbird.io, Version=v1alpha1 + case v1alpha1.SchemeGroupVersion.WithKind("Group"): + return &apiv1alpha1.GroupApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("GroupSpec"): + return &apiv1alpha1.GroupSpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("GroupStatus"): + return &apiv1alpha1.GroupStatusApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("ResourceReference"): + return &apiv1alpha1.ResourceReferenceApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("SetupKey"): return &apiv1alpha1.SetupKeyApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("SetupKeySpec"):