From 2a4d03cbfe6414ea2e22ca672a47c545a2ed83f1 Mon Sep 17 00:00:00 2001 From: Vegard Engen Date: Thu, 10 Apr 2025 00:14:11 +0200 Subject: [PATCH] Working manually configured firewall group entries --- api/v1beta1/firewallgroup_types.go | 89 +++++++++ api/v1beta1/zz_generated.deepcopy.go | 109 ++++++++++ cmd/main.go | 9 + .../unifi.engen.priv.no_firewallgroups.yaml | 130 ++++++++++++ config/rbac/role.yaml | 3 + .../samples/unifi_v1beta1_firewallgroup.yaml | 17 ++ .../controller/firewallgroup_controller.go | 186 ++++++++++++++++++ .../firewallgroup_controller_test.go | 84 ++++++++ 8 files changed, 627 insertions(+) create mode 100644 api/v1beta1/firewallgroup_types.go create mode 100644 config/crd/bases/unifi.engen.priv.no_firewallgroups.yaml create mode 100644 config/samples/unifi_v1beta1_firewallgroup.yaml create mode 100644 internal/controller/firewallgroup_controller.go create mode 100644 internal/controller/firewallgroup_controller_test.go diff --git a/api/v1beta1/firewallgroup_types.go b/api/v1beta1/firewallgroup_types.go new file mode 100644 index 0000000..ee7e36f --- /dev/null +++ b/api/v1beta1/firewallgroup_types.go @@ -0,0 +1,89 @@ +/* +Copyright 2025 Vegard Engen. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// FirewallGroupSpec defines the desired state of FirewallGroup. +type FirewallGroupSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // Foo is an example field of FirewallGroup. Edit firewallgroup_types.go to remove/update + // Description is a human-readable explanation for the object + Name string `json:"name,omitempty"` + + MatchServicesInAllNamespaces bool `json:"matchServicesInAllNamespaces,omitempty"` + // ManualAddresses is a list of manual IPs or CIDRs (IPv4 or IPv6) + // +optional + ManualAddresses []string `json:"manualAddresses,omitempty"` + + // AutoIncludeSelector defines which services to extract addresses from + // +optional + AutoIncludeSelector *metav1.LabelSelector `json:"autoIncludeSelector,omitempty"` + + // AddressType can be "ip", "cidr", or "both" + // +kubebuilder:validation:Enum=ip;cidr;both + // +optional + AddressType string `json:"addressType,omitempty"` +} + +// FirewallGroupStatus defines the observed state of FirewallGroup. +type FirewallGroupStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file + + ResolvedAddresses []string `json:"resolvedAddresses,omitempty"` + + // SyncedWithUnifi indicates whether the addresses are successfully pushed + // +optional + SyncedWithUnifi bool `json:"syncedWithUnifi,omitempty"` + + // LastSyncTime is the last time the object was synced + // +optional + LastSyncTime *metav1.Time `json:"lastSyncTime,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// FirewallGroup is the Schema for the firewallgroups API. +type FirewallGroup struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec FirewallGroupSpec `json:"spec,omitempty"` + Status FirewallGroupStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// FirewallGroupList contains a list of FirewallGroup. +type FirewallGroupList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []FirewallGroup `json:"items"` +} + +func init() { + SchemeBuilder.Register(&FirewallGroup{}, &FirewallGroupList{}) +} diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 8b86052..5cbfa29 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -21,9 +21,118 @@ limitations under the License. package v1beta1 import ( + "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 *FirewallGroup) DeepCopyInto(out *FirewallGroup) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirewallGroup. +func (in *FirewallGroup) DeepCopy() *FirewallGroup { + if in == nil { + return nil + } + out := new(FirewallGroup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FirewallGroup) 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 *FirewallGroupList) DeepCopyInto(out *FirewallGroupList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]FirewallGroup, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirewallGroupList. +func (in *FirewallGroupList) DeepCopy() *FirewallGroupList { + if in == nil { + return nil + } + out := new(FirewallGroupList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FirewallGroupList) 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 *FirewallGroupSpec) DeepCopyInto(out *FirewallGroupSpec) { + *out = *in + if in.ManualAddresses != nil { + in, out := &in.ManualAddresses, &out.ManualAddresses + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.AutoIncludeSelector != nil { + in, out := &in.AutoIncludeSelector, &out.AutoIncludeSelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirewallGroupSpec. +func (in *FirewallGroupSpec) DeepCopy() *FirewallGroupSpec { + if in == nil { + return nil + } + out := new(FirewallGroupSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FirewallGroupStatus) DeepCopyInto(out *FirewallGroupStatus) { + *out = *in + if in.ResolvedAddresses != nil { + in, out := &in.ResolvedAddresses, &out.ResolvedAddresses + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.LastSyncTime != nil { + in, out := &in.LastSyncTime, &out.LastSyncTime + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirewallGroupStatus. +func (in *FirewallGroupStatus) DeepCopy() *FirewallGroupStatus { + if in == nil { + return nil + } + out := new(FirewallGroupStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Networkconfiguration) DeepCopyInto(out *Networkconfiguration) { *out = *in diff --git a/cmd/main.go b/cmd/main.go index 89b07df..6fdfd81 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -223,6 +223,15 @@ func main() { } // +kubebuilder:scaffold:builder + if err = (&controller.FirewallGroupReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + UnifiClient: unifiClient, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "FirewallGroup") + os.Exit(1) + } + if metricsCertWatcher != nil { setupLog.Info("Adding metrics certificate watcher to manager") if err := mgr.Add(metricsCertWatcher); err != nil { diff --git a/config/crd/bases/unifi.engen.priv.no_firewallgroups.yaml b/config/crd/bases/unifi.engen.priv.no_firewallgroups.yaml new file mode 100644 index 0000000..f54583f --- /dev/null +++ b/config/crd/bases/unifi.engen.priv.no_firewallgroups.yaml @@ -0,0 +1,130 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + name: firewallgroups.unifi.engen.priv.no +spec: + group: unifi.engen.priv.no + names: + kind: FirewallGroup + listKind: FirewallGroupList + plural: firewallgroups + singular: firewallgroup + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: FirewallGroup is the Schema for the firewallgroups 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: FirewallGroupSpec defines the desired state of FirewallGroup. + properties: + addressType: + description: AddressType can be "ip", "cidr", or "both" + enum: + - ip + - cidr + - both + type: string + autoIncludeSelector: + description: AutoIncludeSelector defines which services to extract + addresses from + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + manualAddresses: + description: ManualAddresses is a list of manual IPs or CIDRs (IPv4 + or IPv6) + items: + type: string + type: array + matchServicesInAllNamespaces: + type: boolean + name: + description: |- + Foo is an example field of FirewallGroup. Edit firewallgroup_types.go to remove/update + Description is a human-readable explanation for the object + type: string + type: object + status: + description: FirewallGroupStatus defines the observed state of FirewallGroup. + properties: + lastSyncTime: + description: LastSyncTime is the last time the object was synced + format: date-time + type: string + resolvedAddresses: + items: + type: string + type: array + syncedWithUnifi: + description: SyncedWithUnifi indicates whether the addresses are successfully + pushed + type: boolean + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 3c5d379..13aa80e 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -7,6 +7,7 @@ rules: - apiGroups: - unifi.engen.priv.no resources: + - firewallgroups - networkconfigurations verbs: - create @@ -19,12 +20,14 @@ rules: - apiGroups: - unifi.engen.priv.no resources: + - firewallgroups/finalizers - networkconfigurations/finalizers verbs: - update - apiGroups: - unifi.engen.priv.no resources: + - firewallgroups/status - networkconfigurations/status verbs: - get diff --git a/config/samples/unifi_v1beta1_firewallgroup.yaml b/config/samples/unifi_v1beta1_firewallgroup.yaml new file mode 100644 index 0000000..7a99241 --- /dev/null +++ b/config/samples/unifi_v1beta1_firewallgroup.yaml @@ -0,0 +1,17 @@ +apiVersion: unifi.engen.priv.no/v1beta1 +kind: FirewallGroup +metadata: + labels: + app.kubernetes.io/name: unifi-network-operator + app.kubernetes.io/managed-by: kustomize + name: firewallgroup-sample +spec: + name: Test + manualAddresses: + - 192.168.1.153 + - 192.168.1.154 + - 192.168.1.155 + - 2a01::3 + - 2a01:0::5 + - 2a01:2a01::/32 + # TODO(user): Add fields here diff --git a/internal/controller/firewallgroup_controller.go b/internal/controller/firewallgroup_controller.go new file mode 100644 index 0000000..aea91af --- /dev/null +++ b/internal/controller/firewallgroup_controller.go @@ -0,0 +1,186 @@ +/* +Copyright 2025 Vegard Engen. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "net" + "fmt" + "reflect" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + unifiv1beta1 "github.com/vegardengen/unifi-network-operator/api/v1beta1" + goUnifi "github.com/vegardengen/go-unifi/unifi" + "github.com/vegardengen/unifi-network-operator/internal/unifi" +) + +// FirewallGroupReconciler reconciles a FirewallGroup object +type FirewallGroupReconciler struct { + client.Client + Scheme *runtime.Scheme + UnifiClient *unifi.UnifiClient +} + +// +kubebuilder:rbac:groups=unifi.engen.priv.no,resources=firewallgroups,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=unifi.engen.priv.no,resources=firewallgroups/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=unifi.engen.priv.no,resources=firewallgroups/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the FirewallGroup object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.2/pkg/reconcile + + + +func (r *FirewallGroupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := log.FromContext(ctx) + var nwObj unifiv1beta1.FirewallGroup + if err := r.Get(ctx, req.NamespacedName, &nwObj); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + log.Info(nwObj.Spec.Name) + var ipv4, ipv6 []string + + for _,addressEntry := range nwObj.Spec.ManualAddresses { + ip := net.ParseIP(addressEntry) + + if ip != nil { + if ip.To4() != nil { + log.Info(fmt.Sprintf("IPv4 address: %s", addressEntry)) + ipv4 = append(ipv4, addressEntry) + } else { + log.Info(fmt.Sprintf("IPv6 address: %s", addressEntry)) + ipv6 = append(ipv6, ip.String()) + } + } else { + addr, net, err := net.ParseCIDR(addressEntry) + if err == nil && addr.Equal(net.IP) { + if addr.To4() != nil { + log.Info(fmt.Sprintf("Ipv4 Net: %s", net)) + ipv4 = append(ipv4, addressEntry) + } else { + mask,_ := net.Mask.Size() + log.Info(fmt.Sprintf("Ipv6 Net: %s", net)) + ipv6 = append(ipv6, addr.Mask(net.Mask).String() + "/" + fmt.Sprint(mask)) + } + } else { + log.Error(err,fmt.Sprintf("Could not parse: %s", addressEntry)) + return ctrl.Result{}, err + } + } + } + firewall_groups, err := r.UnifiClient.Client.ListFirewallGroup(context.Background(), r.UnifiClient.SiteID) + if err != nil { + log.Error(err,"Could not list network objects") + return ctrl.Result{}, err + } + ipv4_name := "k8s-"+nwObj.Spec.Name+"-ipv4" + ipv6_name := "k8s-"+nwObj.Spec.Name+"-ipv6" + ipv4_done := false + ipv6_done := false + for _,firewall_group := range firewall_groups { + if firewall_group.Name == ipv4_name { + if(len(ipv4) == 0) { + log.Info(fmt.Sprintf("Delete %s", ipv4_name)) + err := r.UnifiClient.Client.DeleteFirewallGroup(context.Background(), r.UnifiClient.SiteID, firewall_group.ID) + if err != nil { + log.Error(err,"Could not delete firewall group") + return ctrl.Result{}, err + } + ipv4_done = true + } else { + if !reflect.DeepEqual(firewall_group.GroupMembers, ipv4) { + firewall_group.GroupMembers = ipv4 + log.Info(fmt.Sprintf("Updating %s", ipv4_name)) + _, err := r.UnifiClient.Client.UpdateFirewallGroup(context.Background(), r.UnifiClient.SiteID, &firewall_group) + if err != nil { + log.Error(err,"Could not update firewall group") + return ctrl.Result{}, err + } + } + ipv4_done = true + } + } + if firewall_group.Name == ipv6_name { + if(len(ipv6) == 0) { + log.Info(fmt.Sprintf("Delete %s", ipv6_name)) + err := r.UnifiClient.Client.DeleteFirewallGroup(context.Background(), r.UnifiClient.SiteID, firewall_group.ID) + if err != nil { + log.Error(err,"Could not delete firewall group") + return ctrl.Result{}, err + } + ipv6_done = true + } else { + if !reflect.DeepEqual(firewall_group.GroupMembers, ipv6) { + firewall_group.GroupMembers = ipv6 + log.Info(fmt.Sprintf("Updating %s", ipv6_name)) + _, err := r.UnifiClient.Client.UpdateFirewallGroup(context.Background(), r.UnifiClient.SiteID, &firewall_group) + if err != nil { + log.Error(err,"Could not update firewall group") + return ctrl.Result{}, err + } + } + ipv6_done = true + } + } + } + if len(ipv4) > 0 && !ipv4_done { + log.Info(fmt.Sprintf("Creating %s", ipv4_name)) + var firewall_group goUnifi.FirewallGroup + firewall_group.Name=ipv4_name + firewall_group.SiteID=r.UnifiClient.SiteID + firewall_group.GroupMembers = ipv4 + firewall_group.GroupType = "address-group" + _, err := r.UnifiClient.Client.CreateFirewallGroup(context.Background(), r.UnifiClient.SiteID, &firewall_group) + if err != nil { + log.Error(err,"Could not create firewall group") + return ctrl.Result{}, err + } + } + if len(ipv6) > 0 && !ipv6_done { + log.Info(fmt.Sprintf("Creating %s", ipv6_name)) + var firewall_group goUnifi.FirewallGroup + firewall_group.Name=ipv6_name + firewall_group.SiteID=r.UnifiClient.SiteID + firewall_group.GroupMembers = ipv6 + firewall_group.GroupType = "ipv6-address-group" + _, err := r.UnifiClient.Client.CreateFirewallGroup(context.Background(), r.UnifiClient.SiteID, &firewall_group) + if err != nil { + log.Error(err,"Could not create firewall group") + return ctrl.Result{}, err + } + } + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *FirewallGroupReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&unifiv1beta1.FirewallGroup{}). + Named("firewallgroup"). + Complete(r) +} diff --git a/internal/controller/firewallgroup_controller_test.go b/internal/controller/firewallgroup_controller_test.go new file mode 100644 index 0000000..ec37660 --- /dev/null +++ b/internal/controller/firewallgroup_controller_test.go @@ -0,0 +1,84 @@ +/* +Copyright 2025 Vegard Engen. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + unifiv1beta1 "github.com/vegardengen/unifi-network-operator/api/v1beta1" +) + +var _ = Describe("FirewallGroup Controller", func() { + Context("When reconciling a resource", func() { + const resourceName = "test-resource" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", // TODO(user):Modify as needed + } + firewallgroup := &unifiv1beta1.FirewallGroup{} + + BeforeEach(func() { + By("creating the custom resource for the Kind FirewallGroup") + err := k8sClient.Get(ctx, typeNamespacedName, firewallgroup) + if err != nil && errors.IsNotFound(err) { + resource := &unifiv1beta1.FirewallGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + // TODO(user): Specify other spec details if needed. + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &unifiv1beta1.FirewallGroup{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance FirewallGroup") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &FirewallGroupReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + 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. + }) + }) +})