From 89a811bef940111913b1ad3af6bb935c79fc1f1b Mon Sep 17 00:00:00 2001 From: Vegard Engen Date: Tue, 15 Apr 2025 15:25:30 +0200 Subject: [PATCH] Preliminary scaffolding --- api/v1beta1/common_types.go | 20 ++++ api/v1beta1/firewallgroup_types.go | 8 +- api/v1beta1/firewallrule_types.go | 22 +++- api/v1beta1/zz_generated.deepcopy.go | 70 ++++++++++- .../unifi.engen.priv.no_firewallgroups.yaml | 16 +-- .../unifi.engen.priv.no_firewallrules.yaml | 41 ++++++- .../samples/unifi_v1beta1_firewallrule.yaml | 1 - .../controller/firewallrule_controller.go | 109 ++++++++++++++++++ 8 files changed, 266 insertions(+), 21 deletions(-) create mode 100644 api/v1beta1/common_types.go diff --git a/api/v1beta1/common_types.go b/api/v1beta1/common_types.go new file mode 100644 index 0000000..b929e31 --- /dev/null +++ b/api/v1beta1/common_types.go @@ -0,0 +1,20 @@ +package v1beta1 + +// 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. + +// FirewallRuleSpec defines the desired state of FirewallRule. +type ServiceSpec struct { + Namespace string `json:"namespace,omitempty"` + Name string `json:"name,omitempty"` +} + +type FirewallSource struct { + Zones []string `json:"from_zones,omitempty"` + Networks []string `json:"from_networks,omitempty"` +} + +type FirewallDestination struct { + FirewallGroups []string `json:"firewall_group,omitempty"` + Services []ServiceSpec `json:"service,omitempty"` +} diff --git a/api/v1beta1/firewallgroup_types.go b/api/v1beta1/firewallgroup_types.go index 7430711..be83790 100644 --- a/api/v1beta1/firewallgroup_types.go +++ b/api/v1beta1/firewallgroup_types.go @@ -24,6 +24,7 @@ import ( // 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 @@ -38,14 +39,11 @@ type FirewallGroupSpec struct { ManualAddresses []string `json:"manualAddresses,omitempty"` ManualPorts []string `json:"manualPorts,omitempty"` + AutoCreatedFrom ServiceSpec `json:"auto_created_from,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. diff --git a/api/v1beta1/firewallrule_types.go b/api/v1beta1/firewallrule_types.go index ad2c724..d77c8aa 100644 --- a/api/v1beta1/firewallrule_types.go +++ b/api/v1beta1/firewallrule_types.go @@ -24,12 +24,30 @@ import ( // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. // FirewallRuleSpec defines the desired state of FirewallRule. +// type ServiceSpec struct { +// Namespace string `json:"namespace,omitempty"` +// Name string `json:"name,omitempty"` +// } + +// type FirewallSource struct { +// Zones []string `json:"from_zones,omitempty"` +// Networks []string `json:"from_networks,omitempty"` +//} + +//type FirewallDestination struct { +// FirewallGroups []string `json:"firewall_group,omitempty"` +// Services []ServiceSpec `json:"service,omitempty"` +//} + type FirewallRuleSpec 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 FirewallRule. Edit firewallrule_types.go to remove/update - Foo string `json:"foo,omitempty"` + Name string `json:"name"` + Source FirewallSource `json:"source"` + Destination FirewallDestination `json:"destination"` + MatchFirewallGroupsInAllNamespaces bool `json:"match_firewall_groups_in_all_namespaces,omitempty"` + MatchServicesInAllNamespaces bool `json:"match_services_in_all_namespaces,omitempty"` } // FirewallRuleStatus defines the observed state of FirewallRule. diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index c79f988..e28d8c0 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -25,6 +25,31 @@ import ( 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 *FirewallDestination) DeepCopyInto(out *FirewallDestination) { + *out = *in + if in.FirewallGroups != nil { + in, out := &in.FirewallGroups, &out.FirewallGroups + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Services != nil { + in, out := &in.Services, &out.Services + *out = make([]ServiceSpec, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirewallDestination. +func (in *FirewallDestination) DeepCopy() *FirewallDestination { + if in == nil { + return nil + } + out := new(FirewallDestination) + in.DeepCopyInto(out) + return out +} + // 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 @@ -97,6 +122,7 @@ func (in *FirewallGroupSpec) DeepCopyInto(out *FirewallGroupSpec) { *out = make([]string, len(*in)) copy(*out, *in) } + out.AutoCreatedFrom = in.AutoCreatedFrom if in.AutoIncludeSelector != nil { in, out := &in.AutoIncludeSelector, &out.AutoIncludeSelector *out = new(v1.LabelSelector) @@ -143,7 +169,7 @@ func (in *FirewallRule) DeepCopyInto(out *FirewallRule) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) out.Status = in.Status } @@ -200,6 +226,8 @@ func (in *FirewallRuleList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FirewallRuleSpec) DeepCopyInto(out *FirewallRuleSpec) { *out = *in + in.Source.DeepCopyInto(&out.Source) + in.Destination.DeepCopyInto(&out.Destination) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirewallRuleSpec. @@ -227,6 +255,31 @@ func (in *FirewallRuleStatus) DeepCopy() *FirewallRuleStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FirewallSource) DeepCopyInto(out *FirewallSource) { + *out = *in + if in.Zones != nil { + in, out := &in.Zones, &out.Zones + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Networks != nil { + in, out := &in.Networks, &out.Networks + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirewallSource. +func (in *FirewallSource) DeepCopy() *FirewallSource { + if in == nil { + return nil + } + out := new(FirewallSource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FirewallZone) DeepCopyInto(out *FirewallZone) { *out = *in @@ -413,3 +466,18 @@ func (in *NetworkconfigurationStatus) DeepCopy() *NetworkconfigurationStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceSpec) DeepCopyInto(out *ServiceSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceSpec. +func (in *ServiceSpec) DeepCopy() *ServiceSpec { + if in == nil { + return nil + } + out := new(ServiceSpec) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/unifi.engen.priv.no_firewallgroups.yaml b/config/crd/bases/unifi.engen.priv.no_firewallgroups.yaml index b9cf679..54e7f4b 100644 --- a/config/crd/bases/unifi.engen.priv.no_firewallgroups.yaml +++ b/config/crd/bases/unifi.engen.priv.no_firewallgroups.yaml @@ -37,15 +37,15 @@ spec: 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 + auto_created_from: + description: FirewallRuleSpec defines the desired state of FirewallRule. + properties: + name: + type: string + namespace: + type: string + type: object autoIncludeSelector: description: AutoIncludeSelector defines which services to extract addresses from diff --git a/config/crd/bases/unifi.engen.priv.no_firewallrules.yaml b/config/crd/bases/unifi.engen.priv.no_firewallrules.yaml index 3a03cb9..95e9212 100644 --- a/config/crd/bases/unifi.engen.priv.no_firewallrules.yaml +++ b/config/crd/bases/unifi.engen.priv.no_firewallrules.yaml @@ -37,12 +37,45 @@ spec: metadata: type: object spec: - description: FirewallRuleSpec defines the desired state of FirewallRule. properties: - foo: - description: Foo is an example field of FirewallRule. Edit firewallrule_types.go - to remove/update + destination: + properties: + firewall_group: + items: + type: string + type: array + service: + items: + description: FirewallRuleSpec defines the desired state of FirewallRule. + properties: + name: + type: string + namespace: + type: string + type: object + type: array + type: object + match_firewall_groups_in_all_namespaces: + type: boolean + match_services_in_all_namespaces: + type: boolean + name: type: string + source: + properties: + from_networks: + items: + type: string + type: array + from_zones: + items: + type: string + type: array + type: object + required: + - destination + - name + - source type: object status: description: FirewallRuleStatus defines the observed state of FirewallRule. diff --git a/config/samples/unifi_v1beta1_firewallrule.yaml b/config/samples/unifi_v1beta1_firewallrule.yaml index 035b46b..b1c287c 100644 --- a/config/samples/unifi_v1beta1_firewallrule.yaml +++ b/config/samples/unifi_v1beta1_firewallrule.yaml @@ -6,4 +6,3 @@ metadata: app.kubernetes.io/managed-by: kustomize name: firewallrule-sample spec: - # TODO(user): Add fields here diff --git a/internal/controller/firewallrule_controller.go b/internal/controller/firewallrule_controller.go index 10dfd5a..badd2cf 100644 --- a/internal/controller/firewallrule_controller.go +++ b/internal/controller/firewallrule_controller.go @@ -18,10 +18,14 @@ package controller import ( "context" + "time" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" unifiv1beta1 "github.com/vegardengen/unifi-network-operator/api/v1beta1" @@ -41,6 +45,7 @@ type FirewallRuleReconciler struct { // +kubebuilder:rbac:groups=unifi.engen.priv.no,resources=firewallrules/status,verbs=get;update;patch // +kubebuilder:rbac:groups=unifi.engen.priv.no,resources=firewallrules/finalizers,verbs=update // +kubebuilder:rbac:groups="",resources=configmaps,verbs=list;get;watch +// +kubebuilder:rbac:groups="",resources=services,verbs=list;get;watch // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -69,13 +74,117 @@ func (r *FirewallRuleReconciler) Reconcile(ctx context.Context, req ctrl.Request return ctrl.Result{}, err } + var zoneCRDs unifiv1beta1.FirewallZoneList + var networkCRDs unifiv1beta1.NetworkconfigurationList + + err = r.List(ctx, &zoneCRDs, client.InNamespace(defaultNs)) + if err != nil { + log.Error(err, "Could not list firewall zones") + return ctrl.Result{RequeueAfter: 10 * time.Minute}, err + } + + zoneCRDNames := make(map[string]struct{}) + + for _, zoneCRD := range zoneCRDs.Items { + zoneCRDNames[zoneCRD.Name] = struct{}{} + } + + err = r.List(ctx, &networkCRDs, client.InNamespace(defaultNs)) + if err != nil { + log.Error(err, "Could not list networks") + return ctrl.Result{RequeueAfter: 10 * time.Minute}, err + } + + networkCRDNames := make(map[string]struct{}) + + for _, networkCRD := range networkCRDs.Items { + networkCRDNames[networkCRD.Name] = struct{}{} + } + + var firewallRule unifiv1beta1.FirewallRule + + if err := r.Get(ctx, req.NamespacedName, &firewallRule); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + log.Info(firewallRule.Spec.Name) + return ctrl.Result{}, nil } +func (r *FirewallRuleReconciler) mapFirewallGroupToFirewallRules(ctx context.Context, obj client.Object) []ctrl.Request { + var requests []ctrl.Request + service, ok := obj.(*corev1.Service) + if !ok { + return requests + } + + var allFirewallRules unifiv1beta1.FirewallRuleList + + if err := r.List(ctx, &allFirewallRules); err != nil { + return nil + } + + for _, rule := range allFirewallRules.Items { + if rule.Spec.MatchFirewallGroupsInAllNamespaces || rule.Namespace == service.Namespace { + annotationKey := "unifi.engen.priv.no/firewall-rule" + annotationVal := rule.Name + if val, ok := service.Annotations[annotationKey]; ok && (annotationVal == "" || val == annotationVal) { + requests = append(requests, ctrl.Request{ + NamespacedName: types.NamespacedName{ + Name: rule.Name, + Namespace: rule.Namespace, + }, + }) + } + } + } + + return requests +} + +func (r *FirewallRuleReconciler) mapServiceToFirewallRules(ctx context.Context, obj client.Object) []ctrl.Request { + var requests []ctrl.Request + service, ok := obj.(*corev1.Service) + if !ok { + return requests + } + + var allFirewallRules unifiv1beta1.FirewallRuleList + + if err := r.List(ctx, &allFirewallRules); err != nil { + return nil + } + + for _, rule := range allFirewallRules.Items { + if rule.Spec.MatchServicesInAllNamespaces || rule.Namespace == service.Namespace { + annotationKey := "unifi.engen.priv.no/firewall-rule" + annotationVal := rule.Name + if val, ok := service.Annotations[annotationKey]; ok && (annotationVal == "" || val == annotationVal) { + requests = append(requests, ctrl.Request{ + NamespacedName: types.NamespacedName{ + Name: rule.Name, + Namespace: rule.Namespace, + }, + }) + } + } + } + + return requests +} + // SetupWithManager sets up the controller with the Manager. func (r *FirewallRuleReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&unifiv1beta1.FirewallRule{}). Named("firewallrule"). + Watches( + &corev1.Service{}, + handler.EnqueueRequestsFromMapFunc(r.mapServiceToFirewallRules), + ). + Watches( + &unifiv1beta1.FirewallGroup{}, + handler.EnqueueRequestsFromMapFunc(r.mapFirewallGroupToFirewallRules), + ). Complete(r) }