diff --git a/PROJECT b/PROJECT index e0bb323d2..40926c22d 100644 --- a/PROJECT +++ b/PROJECT @@ -163,8 +163,8 @@ resources: path: github.com/linode/cluster-api-provider-linode/api/v1alpha2 version: v1alpha2 webhooks: - validation: true defaulting: true + validation: true webhookVersion: v1 - api: crdVersion: v1 @@ -178,4 +178,12 @@ resources: webhooks: validation: true webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + domain: cluster.x-k8s.io + group: infrastructure + kind: AddressSet + path: github.com/linode/cluster-api-provider-linode/api/v1alpha2 + version: v1alpha2 version: "3" diff --git a/api/v1alpha2/addressset_types.go b/api/v1alpha2/addressset_types.go new file mode 100644 index 000000000..c7eef651c --- /dev/null +++ b/api/v1alpha2/addressset_types.go @@ -0,0 +1,66 @@ +/* +Copyright 2023 Akamai Technologies, Inc. + +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 v1alpha2 + +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. + +// AddressSetSpec defines the desired state of AddressSet +type AddressSetSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + IPv4 *[]string `json:"ipv4,omitempty"` + IPv6 *[]string `json:"ipv6,omitempty"` +} + +// AddressSetStatus defines the observed state of AddressSet +type AddressSetStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:path=addressset,scope=Namespaced,categories=cluster-api,shortName=addrset +// +kubebuilder:metadata:labels="clusterctl.cluster.x-k8s.io/move-hierarchy=true" + +// AddressSet is the Schema for the addresssets API +type AddressSet struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AddressSetSpec `json:"spec,omitempty"` + Status AddressSetStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// AddressSetList contains a list of AddressSet +type AddressSetList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []AddressSet `json:"items"` +} + +func init() { + SchemeBuilder.Register(&AddressSet{}, &AddressSetList{}) +} diff --git a/api/v1alpha2/linodefirewall_types.go b/api/v1alpha2/linodefirewall_types.go index fcdad4252..03b1d593e 100644 --- a/api/v1alpha2/linodefirewall_types.go +++ b/api/v1alpha2/linodefirewall_types.go @@ -70,6 +70,9 @@ type FirewallRule struct { // +kubebuilder:validation:Enum=TCP;UDP;ICMP;IPENCAP Protocol linodego.NetworkProtocol `json:"protocol"` Addresses *NetworkAddresses `json:"addresses"` + // AddressSetRefs is a list of references to AddressSets + // If Addresses is present, AddressSetRefs will be ignored + AddressSetRefs []*corev1.ObjectReference `json:"addressSetRefs,omitempty"` } // NetworkAddresses holds a list of IPv4 and IPv6 addresses diff --git a/api/v1alpha2/zz_generated.deepcopy.go b/api/v1alpha2/zz_generated.deepcopy.go index 3978e4df7..21dff695d 100644 --- a/api/v1alpha2/zz_generated.deepcopy.go +++ b/api/v1alpha2/zz_generated.deepcopy.go @@ -28,6 +28,113 @@ import ( "sigs.k8s.io/cluster-api/errors" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AddressSet) DeepCopyInto(out *AddressSet) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AddressSet. +func (in *AddressSet) DeepCopy() *AddressSet { + if in == nil { + return nil + } + out := new(AddressSet) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AddressSet) 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 *AddressSetList) DeepCopyInto(out *AddressSetList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AddressSet, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AddressSetList. +func (in *AddressSetList) DeepCopy() *AddressSetList { + if in == nil { + return nil + } + out := new(AddressSetList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AddressSetList) 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 *AddressSetSpec) DeepCopyInto(out *AddressSetSpec) { + *out = *in + if in.IPv4 != nil { + in, out := &in.IPv4, &out.IPv4 + *out = new([]string) + if **in != nil { + in, out := *in, *out + *out = make([]string, len(*in)) + copy(*out, *in) + } + } + if in.IPv6 != nil { + in, out := &in.IPv6, &out.IPv6 + *out = new([]string) + if **in != nil { + in, out := *in, *out + *out = make([]string, len(*in)) + copy(*out, *in) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AddressSetSpec. +func (in *AddressSetSpec) DeepCopy() *AddressSetSpec { + if in == nil { + return nil + } + out := new(AddressSetSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AddressSetStatus) DeepCopyInto(out *AddressSetStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AddressSetStatus. +func (in *AddressSetStatus) DeepCopy() *AddressSetStatus { + if in == nil { + return nil + } + out := new(AddressSetStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BucketAccessRef) DeepCopyInto(out *BucketAccessRef) { *out = *in @@ -51,6 +158,17 @@ func (in *FirewallRule) DeepCopyInto(out *FirewallRule) { *out = new(NetworkAddresses) (*in).DeepCopyInto(*out) } + if in.AddressSetRefs != nil { + in, out := &in.AddressSetRefs, &out.AddressSetRefs + *out = make([]*v1.ObjectReference, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(v1.ObjectReference) + **out = **in + } + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirewallRule. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_addressset.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_addressset.yaml new file mode 100644 index 000000000..e07eca965 --- /dev/null +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_addressset.yaml @@ -0,0 +1,64 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.5 + labels: + clusterctl.cluster.x-k8s.io/move-hierarchy: "true" + name: addressset.infrastructure.cluster.x-k8s.io +spec: + group: infrastructure.cluster.x-k8s.io + names: + categories: + - cluster-api + kind: AddressSet + listKind: AddressSetList + plural: addressset + shortNames: + - addrset + singular: addressset + scope: Namespaced + versions: + - name: v1alpha2 + schema: + openAPIV3Schema: + description: AddressSet is the Schema for the addresssets 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: AddressSetSpec defines the desired state of AddressSet + properties: + ipv4: + items: + type: string + type: array + ipv6: + items: + type: string + type: array + type: object + status: + description: AddressSetStatus defines the observed state of AddressSet + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_addresssets.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_addresssets.yaml new file mode 100644 index 000000000..46f0cd41c --- /dev/null +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_addresssets.yaml @@ -0,0 +1,54 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.5 + name: addresssets.infrastructure.cluster.x-k8s.io +spec: + group: infrastructure.cluster.x-k8s.io + names: + kind: AddressSet + listKind: AddressSetList + plural: addresssets + singular: addressset + scope: Namespaced + versions: + - name: v1alpha2 + schema: + openAPIV3Schema: + description: AddressSet is the Schema for the addresssets 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: AddressSetSpec defines the desired state of AddressSet + properties: + foo: + description: Foo is an example field of AddressSet. Edit addressset_types.go + to remove/update + type: string + type: object + status: + description: AddressSetStatus defines the observed state of AddressSet + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodefirewalls.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodefirewalls.yaml index a048ba319..e67ac0b6b 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodefirewalls.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodefirewalls.yaml @@ -83,6 +83,55 @@ spec: properties: action: type: string + addressSetRefs: + description: |- + AddressSetRefs is a list of references to AddressSets + If Addresses is present, AddressSetRefs will be ignored + items: + description: ObjectReference contains enough information to + let you inspect or modify the referred object. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + type: array addresses: description: |- NetworkAddresses holds a list of IPv4 and IPv6 addresses @@ -132,6 +181,55 @@ spec: properties: action: type: string + addressSetRefs: + description: |- + AddressSetRefs is a list of references to AddressSets + If Addresses is present, AddressSetRefs will be ignored + items: + description: ObjectReference contains enough information to + let you inspect or modify the referred object. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + type: array addresses: description: |- NetworkAddresses holds a list of IPv4 and IPv6 addresses diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index ae0964ce0..8a099af4f 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -18,6 +18,7 @@ resources: - bases/infrastructure.cluster.x-k8s.io_linodeplacementgroups.yaml - bases/infrastructure.cluster.x-k8s.io_linodeobjectstoragekeys.yaml - bases/infrastructure.cluster.x-k8s.io_linodefirewalls.yaml +- bases/infrastructure.cluster.x-k8s.io_addresssets.yaml #+kubebuilder:scaffold:crdkustomizeresource patches: @@ -38,6 +39,7 @@ patches: - path: patches/webhook_in_linodeclustertemplates.yaml - path: patches/webhook_in_linodemachinetemplates.yaml #- path: patches/webhook_in_linodefirewalls.yaml +#- path: patches/webhook_in_addresssets.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch - path: patches/capicontract_in_linodeclusters.yaml @@ -60,6 +62,7 @@ patches: #- path: patches/cainjection_in_linodevpcs.yaml #- path: patches/cainjection_in_linodeobjectstoragebuckets.yaml #- path: patches/cainjection_in_linodefirewalls.yaml +#- path: patches/cainjection_in_addresssets.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # [VALIDATION] diff --git a/config/crd/patches/cainjection_in_addresssets.yaml b/config/crd/patches/cainjection_in_addresssets.yaml new file mode 100644 index 000000000..dd0f76d7f --- /dev/null +++ b/config/crd/patches/cainjection_in_addresssets.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: addresssets.infrastructure.cluster.x-k8s.io diff --git a/config/crd/patches/webhook_in_addresssets.yaml b/config/crd/patches/webhook_in_addresssets.yaml new file mode 100644 index 000000000..047a9aef7 --- /dev/null +++ b/config/crd/patches/webhook_in_addresssets.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: addresssets.infrastructure.cluster.x-k8s.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/rbac/addressset_editor_role.yaml b/config/rbac/addressset_editor_role.yaml new file mode 100644 index 000000000..26e7f868a --- /dev/null +++ b/config/rbac/addressset_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit addresssets. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: addressset-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: cluster-api-provider-linode + app.kubernetes.io/part-of: cluster-api-provider-linode + app.kubernetes.io/managed-by: kustomize + name: addressset-editor-role +rules: +- apiGroups: + - infrastructure.cluster.x-k8s.io + resources: + - addresssets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - infrastructure.cluster.x-k8s.io + resources: + - addresssets/status + verbs: + - get diff --git a/config/rbac/addressset_viewer_role.yaml b/config/rbac/addressset_viewer_role.yaml new file mode 100644 index 000000000..ef70a0787 --- /dev/null +++ b/config/rbac/addressset_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view addresssets. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: addressset-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: cluster-api-provider-linode + app.kubernetes.io/part-of: cluster-api-provider-linode + app.kubernetes.io/managed-by: kustomize + name: addressset-viewer-role +rules: +- apiGroups: + - infrastructure.cluster.x-k8s.io + resources: + - addresssets + verbs: + - get + - list + - watch +- apiGroups: + - infrastructure.cluster.x-k8s.io + resources: + - addresssets/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 89b1fe55b..673703dec 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -26,6 +26,14 @@ rules: - get - list - watch +- apiGroups: + - infrastructure.cluster.x-k8s.io + resources: + - addresssets + verbs: + - get + - list + - watch - apiGroups: - infrastructure.cluster.x-k8s.io resources: diff --git a/config/samples/infrastructure_v1alpha2_addressset.yaml b/config/samples/infrastructure_v1alpha2_addressset.yaml new file mode 100644 index 000000000..4f48a7095 --- /dev/null +++ b/config/samples/infrastructure_v1alpha2_addressset.yaml @@ -0,0 +1,12 @@ +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2 +kind: AddressSet +metadata: + labels: + app.kubernetes.io/name: addressset + app.kubernetes.io/instance: addressset-sample + app.kubernetes.io/part-of: cluster-api-provider-linode + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: cluster-api-provider-linode + name: addressset-sample +spec: + # TODO(user): Add fields here diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 4218315a0..137110087 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -14,4 +14,5 @@ resources: - infrastructure_v1alpha2_linodeclustertemplate.yaml - infrastructure_v1alpha2_linodemachinetemplate.yaml - infrastructure_v1alpha2_linodefirewall.yaml +- infrastructure_v1alpha2_addressset.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/internal/controller/linodefirewall_controller.go b/internal/controller/linodefirewall_controller.go index 9924a498d..cd910f440 100644 --- a/internal/controller/linodefirewall_controller.go +++ b/internal/controller/linodefirewall_controller.go @@ -61,6 +61,7 @@ type LinodeFirewallReconciler struct { //+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=linodefirewalls,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=linodefirewalls/status,verbs=get;update;patch //+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=linodefirewalls/finalizers,verbs=update +//+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=addresssets,verbs=get;list;watch func (r *LinodeFirewallReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultedLoopTimeout(r.ReconcileTimeout)) @@ -169,7 +170,7 @@ func (r *LinodeFirewallReconciler) reconcile( return ctrl.Result{}, nil } } - if err = reconcileFirewall(ctx, fwScope, logger); err != nil { + if err = reconcileFirewall(ctx, r.Client, fwScope, logger); err != nil { logger.Error(err, fmt.Sprintf("failed to %s Firewall", action)) reconciler.RecordDecayingCondition( fwScope.LinodeFirewall, @@ -250,6 +251,14 @@ func (r *LinodeFirewallReconciler) SetupWithManager(mgr ctrl.Manager, options cr if err != nil { return fmt.Errorf("failed to create mapper for LinodeFirewalls: %w", err) } + addressSetMapper, err := kutil.ClusterToTypedObjectsMapper( + r.TracedClient(), + &infrav1alpha2.AddressSetList{}, + mgr.GetScheme(), + ) + if err != nil { + return fmt.Errorf("failed to create mapper for AddressSets: %w", err) + } err = ctrl.NewControllerManagedBy(mgr). For(&infrav1alpha2.LinodeFirewall{}). WithOptions(options). @@ -266,11 +275,18 @@ func (r *LinodeFirewallReconciler) SetupWithManager(mgr ctrl.Manager, options cr } return true }}, - )).Watches( - &clusterv1.Cluster{}, - handler.EnqueueRequestsFromMapFunc(linodeFirewallMapper), - builder.WithPredicates(predicates.ClusterUnpausedAndInfrastructureReady(mgr.GetLogger())), - ).Complete(wrappedruntimereconciler.NewRuntimeReconcilerWithTracing(r, wrappedruntimereconciler.DefaultDecorator())) + )). + Watches( + &clusterv1.Cluster{}, + handler.EnqueueRequestsFromMapFunc(linodeFirewallMapper), + builder.WithPredicates(predicates.ClusterUnpausedAndInfrastructureReady(mgr.GetLogger())), + ). + Watches( + &infrav1alpha2.AddressSet{}, + handler.EnqueueRequestsFromMapFunc(addressSetMapper), + builder.WithPredicates(predicates.ClusterUnpausedAndInfrastructureReady(mgr.GetLogger())), + ). + Complete(wrappedruntimereconciler.NewRuntimeReconcilerWithTracing(r, wrappedruntimereconciler.DefaultDecorator())) if err != nil { return fmt.Errorf("failed to build controller: %w", err) } diff --git a/internal/controller/linodefirewall_controller_helpers.go b/internal/controller/linodefirewall_controller_helpers.go index f3f02fabe..6f70411b2 100644 --- a/internal/controller/linodefirewall_controller_helpers.go +++ b/internal/controller/linodefirewall_controller_helpers.go @@ -9,6 +9,8 @@ import ( "github.com/go-logr/logr" "github.com/linode/linodego" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" infrav1alpha2 "github.com/linode/cluster-api-provider-linode/api/v1alpha2" "github.com/linode/cluster-api-provider-linode/clients" @@ -30,11 +32,12 @@ var ( // via the given linode client func reconcileFirewall( ctx context.Context, + k8sClient clients.K8sClient, fwScope *scope.FirewallScope, logger logr.Logger, ) error { // build out the firewall rules for create or update - fwConfig, err := processACL(fwScope.LinodeFirewall) + fwConfig, err := processACL(ctx, k8sClient, logger, fwScope.LinodeFirewall) if err != nil { logger.Info("Failed to process ACL", "error", err.Error()) @@ -162,8 +165,14 @@ func transformToCIDR(ip string) string { } // processInboundRule handles a single inbound rule -func processInboundRule(rule infrav1alpha2.FirewallRule, createOpts *linodego.FirewallCreateOptions) { - ruleIPv4s, ruleIPv6s := processAddresses(rule.Addresses) +func processInboundRule(ctx context.Context, k8sClient clients.K8sClient, log logr.Logger, rule infrav1alpha2.FirewallRule, createOpts *linodego.FirewallCreateOptions) { + var ruleIPv4s []string + var ruleIPv6s []string + if rule.Addresses != nil { + ruleIPv4s, ruleIPv6s = processAddresses(rule.Addresses) + } else if rule.AddressSetRefs != nil { + ruleIPv4s, ruleIPv6s = processAddressSetRefs(ctx, k8sClient, rule.AddressSetRefs, log) + } ruleLabel := formatRuleLabel(rule.Action, rule.Label) // Process IPv4 @@ -174,8 +183,14 @@ func processInboundRule(rule infrav1alpha2.FirewallRule, createOpts *linodego.Fi } // processOutboundRule handles a single outbound rule -func processOutboundRule(rule infrav1alpha2.FirewallRule, outboundPolicy string, createOpts *linodego.FirewallCreateOptions) { - ruleIPv4s, ruleIPv6s := processAddresses(rule.Addresses) +func processOutboundRule(ctx context.Context, k8sClient clients.K8sClient, log logr.Logger, rule infrav1alpha2.FirewallRule, outboundPolicy string, createOpts *linodego.FirewallCreateOptions) { + var ruleIPv4s []string + var ruleIPv6s []string + if rule.Addresses != nil { + ruleIPv4s, ruleIPv6s = processAddresses(rule.Addresses) + } else if rule.AddressSetRefs != nil { + ruleIPv4s, ruleIPv6s = processAddressSetRefs(ctx, k8sClient, rule.AddressSetRefs, log) + } ruleLabel := formatRuleLabel(outboundPolicy, rule.Label) // Process IPv4 @@ -213,6 +228,35 @@ func processAddresses(addresses *infrav1alpha2.NetworkAddresses) (ipv4s, ipv6s [ return ipv4s, ipv6s } +// processAddressSetRefs extracts and transforms IPv4 and IPv6 addresses from the reference AddressSet(s) +func processAddressSetRefs(ctx context.Context, k8sClient clients.K8sClient, addressSetRefs []*corev1.ObjectReference, log logr.Logger) (ipv4s, ipv6s []string) { + // Initialize empty slices for consistent return type + ipv4s = make([]string, 0) + ipv6s = make([]string, 0) + + for _, addrSetRef := range addressSetRefs { + addrSet := &infrav1alpha2.AddressSet{} + if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: addrSetRef.Namespace, Name: addrSetRef.Name}, addrSet); err != nil { + log.Error(err, "failed to fetch referenced AddressSet", "namespace", addrSetRef.Namespace, "name", addrSetRef.Name) + } + // Process IPv4 addresses + if addrSet.Spec.IPv4 != nil { + for _, ip := range *addrSet.Spec.IPv4 { + ipv4s = append(ipv4s, transformToCIDR(ip)) + } + } + + // Process IPv6 addresses + if addrSet.Spec.IPv6 != nil { + for _, ip := range *addrSet.Spec.IPv6 { + ipv6s = append(ipv6s, transformToCIDR(ip)) + } + } + } + + return ipv4s, ipv6s +} + // formatRuleLabel creates and formats the rule label func formatRuleLabel(prefix, label string) string { ruleLabel := fmt.Sprintf("%s-%s", prefix, label) @@ -276,14 +320,14 @@ func processIPv6Rules(ips []string, rule infrav1alpha2.FirewallRule, ruleLabel s // processACL uses the CAPL LinodeFirewall representation to build out the inbound // and outbound rules for a linode Cloud Firewall -func processACL(firewall *infrav1alpha2.LinodeFirewall) (*linodego.FirewallCreateOptions, error) { +func processACL(ctx context.Context, k8sClient clients.K8sClient, log logr.Logger, firewall *infrav1alpha2.LinodeFirewall) (*linodego.FirewallCreateOptions, error) { createOpts := &linodego.FirewallCreateOptions{ Label: firewall.Name, } // Process inbound rules for _, rule := range firewall.Spec.InboundRules { - processInboundRule(rule, createOpts) + processInboundRule(ctx, k8sClient, log, rule, createOpts) } // Set inbound policy @@ -295,7 +339,7 @@ func processACL(firewall *infrav1alpha2.LinodeFirewall) (*linodego.FirewallCreat // Process outbound rules for _, rule := range firewall.Spec.OutboundRules { - processOutboundRule(rule, firewall.Spec.OutboundPolicy, createOpts) + processOutboundRule(ctx, k8sClient, log, rule, firewall.Spec.OutboundPolicy, createOpts) } // Set outbound policy diff --git a/internal/controller/linodefirewall_controller_helpers_test.go b/internal/controller/linodefirewall_controller_helpers_test.go index f77cd4474..b28f83f59 100644 --- a/internal/controller/linodefirewall_controller_helpers_test.go +++ b/internal/controller/linodefirewall_controller_helpers_test.go @@ -1,9 +1,11 @@ package controller import ( + "context" "reflect" "testing" + "github.com/go-logr/logr" "github.com/linode/linodego" infrav1alpha2 "github.com/linode/cluster-api-provider-linode/api/v1alpha2" @@ -161,7 +163,9 @@ func TestProcessACL(t *testing.T) { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - got, err := processACL(tt.firewall) + logger := logr.Logger{} + + got, err := processACL(context.Background(), k8sClient, logger, tt.firewall) if (err != nil) != tt.wantErr { t.Errorf("processACL() error = %v, wantErr %v", err, tt.wantErr) return @@ -519,7 +523,8 @@ func TestProcessInboundRule(t *testing.T) { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - processInboundRule(tt.rule, tt.createOpts) + logger := logr.Logger{} + processInboundRule(context.Background(), k8sClient, logger, tt.rule, tt.createOpts) if !reflect.DeepEqual(tt.createOpts, tt.want) { t.Errorf("processInboundRule() = %v, want %v", tt.createOpts, tt.want) } @@ -582,7 +587,8 @@ func TestProcessOutboundRule(t *testing.T) { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - processOutboundRule(tt.rule, tt.outboundPolicy, tt.createOpts) + logger := logr.Logger{} + processOutboundRule(context.Background(), k8sClient, logger, tt.rule, tt.outboundPolicy, tt.createOpts) if !reflect.DeepEqual(tt.createOpts, tt.want) { t.Errorf("processOutboundRule() = %v, want %v", tt.createOpts, tt.want) }