diff --git a/api/v1alpha1/acl_types.go b/api/v1alpha1/acl_types.go index 6e9b7d66..8cd62b42 100644 --- a/api/v1alpha1/acl_types.go +++ b/api/v1alpha1/acl_types.go @@ -142,6 +142,18 @@ func (acl *AccessControlList) SetConditions(conditions []metav1.Condition) { acl.Status.Conditions = conditions } +func (acl *AccessControlList) GetDeviceRef() LocalObjectReference { + return acl.Spec.DeviceRef +} + +func (acl *AccessControlList) GetProviderConfigRef() *TypedLocalObjectReference { + return acl.Spec.ProviderConfigRef +} + +func (acl *AccessControlList) GetStatus() any { + return acl.Status +} + // +kubebuilder:object:root=true // AccessControlListList contains a list of AccessControlList diff --git a/internal/controller/acl_controller.go b/internal/controller/acl_controller.go index d42121ae..9c9857bd 100644 --- a/internal/controller/acl_controller.go +++ b/internal/controller/acl_controller.go @@ -7,9 +7,6 @@ import ( "context" "fmt" - "k8s.io/apimachinery/pkg/api/equality" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" kerrors "k8s.io/apimachinery/pkg/util/errors" @@ -21,7 +18,6 @@ import ( "github.com/ironcore-dev/network-operator/api/v1alpha1" "github.com/ironcore-dev/network-operator/internal/conditions" - "github.com/ironcore-dev/network-operator/internal/deviceutil" "github.com/ironcore-dev/network-operator/internal/provider" ) @@ -46,129 +42,6 @@ type AccessControlListReconciler struct { // +kubebuilder:rbac:groups=networking.cloud.sap,resources=accesscontrollists/finalizers,verbs=update // +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.2/pkg/reconcile -// -// For more details about the method shape, read up here: -// - https://ahmet.im/blog/controller-pitfalls/#reconcile-method-shape -func (r *AccessControlListReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { - log := ctrl.LoggerFrom(ctx) - log.Info("Reconciling resource") - - obj := new(v1alpha1.AccessControlList) - if err := r.Get(ctx, req.NamespacedName, obj); err != nil { - if apierrors.IsNotFound(err) { - // If the custom resource is not found then it usually means that it was deleted or not created - // In this way, we will stop the reconciliation - log.Info("Resource not found. Ignoring since object must be deleted") - return ctrl.Result{}, nil - } - // Error reading the object - requeue the request. - log.Error(err, "Failed to get resource") - return ctrl.Result{}, err - } - - prov, ok := r.Provider().(provider.ACLProvider) - if !ok { - if meta.SetStatusCondition(&obj.Status.Conditions, metav1.Condition{ - Type: v1alpha1.ReadyCondition, - Status: metav1.ConditionFalse, - Reason: v1alpha1.NotImplementedReason, - Message: "Provider does not implement provider.AccessControlListProvider", - }) { - return ctrl.Result{}, r.Status().Update(ctx, obj) - } - return ctrl.Result{}, nil - } - - device, err := deviceutil.GetDeviceByName(ctx, r, obj.Namespace, obj.Spec.DeviceRef.Name) - if err != nil { - return ctrl.Result{}, err - } - - conn, err := deviceutil.GetDeviceConnection(ctx, r, device) - if err != nil { - return ctrl.Result{}, err - } - - var cfg *provider.ProviderConfig - if obj.Spec.ProviderConfigRef != nil { - cfg, err = provider.GetProviderConfig(ctx, r, obj.Namespace, obj.Spec.ProviderConfigRef) - if err != nil { - return ctrl.Result{}, err - } - } - - s := &aclScope{ - Device: device, - ACL: obj, - Connection: conn, - ProviderConfig: cfg, - Provider: prov, - } - - if !obj.DeletionTimestamp.IsZero() { - if controllerutil.ContainsFinalizer(obj, v1alpha1.FinalizerName) { - if err := r.finalize(ctx, s); err != nil { - log.Error(err, "Failed to finalize resource") - return ctrl.Result{}, err - } - controllerutil.RemoveFinalizer(obj, v1alpha1.FinalizerName) - if err := r.Update(ctx, obj); err != nil { - log.Error(err, "Failed to remove finalizer from resource") - return ctrl.Result{}, err - } - } - log.Info("Resource is being deleted, skipping reconciliation") - return ctrl.Result{}, nil - } - - // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers - if !controllerutil.ContainsFinalizer(obj, v1alpha1.FinalizerName) { - controllerutil.AddFinalizer(obj, v1alpha1.FinalizerName) - if err := r.Update(ctx, obj); err != nil { - log.Error(err, "Failed to add finalizer to resource") - return ctrl.Result{}, err - } - log.Info("Added finalizer to resource") - return ctrl.Result{}, nil - } - - orig := obj.DeepCopy() - if conditions.InitializeConditions(obj, v1alpha1.ReadyCondition) { - log.Info("Initializing status conditions") - return ctrl.Result{}, r.Status().Update(ctx, obj) - } - - // Always attempt to update the metadata/status after reconciliation - defer func() { - if !equality.Semantic.DeepEqual(orig.ObjectMeta, obj.ObjectMeta) { - if err := r.Patch(ctx, obj, client.MergeFrom(orig)); err != nil { - log.Error(err, "Failed to update resource metadata") - reterr = kerrors.NewAggregate([]error{reterr, err}) - } - return - } - - if !equality.Semantic.DeepEqual(orig.Status, obj.Status) { - if err := r.Status().Patch(ctx, obj, client.MergeFrom(orig)); err != nil { - log.Error(err, "Failed to update status") - reterr = kerrors.NewAggregate([]error{reterr, err}) - } - } - }() - - if err := r.reconcile(ctx, s); err != nil { - log.Error(err, "Failed to reconcile resource") - return ctrl.Result{}, err - } - - return ctrl.Result{}, nil -} - // SetupWithManager sets up the controller with the Manager. func (r *AccessControlListReconciler) SetupWithManager(mgr ctrl.Manager) error { labelSelector := metav1.LabelSelector{} @@ -181,32 +54,24 @@ func (r *AccessControlListReconciler) SetupWithManager(mgr ctrl.Manager) error { return fmt.Errorf("failed to create label selector predicate: %w", err) } + rec := AsReconciler(r.Client, r.Provider, r) return ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.AccessControlList{}). Named("accesscontrollist"). WithEventFilter(filter). - Complete(r) -} - -// scope holds the different objects that are read and used during the reconcile. -type aclScope struct { - Device *v1alpha1.Device - ACL *v1alpha1.AccessControlList - Connection *deviceutil.Connection - ProviderConfig *provider.ProviderConfig - Provider provider.ACLProvider + Complete(rec) } -func (r *AccessControlListReconciler) reconcile(ctx context.Context, s *aclScope) (reterr error) { - if s.ACL.Labels == nil { - s.ACL.Labels = make(map[string]string) +func (r *AccessControlListReconciler) Reconcile(ctx context.Context, s *TypedScope[*v1alpha1.AccessControlList, provider.ACLProvider]) (reterr error) { + if s.Resource.Labels == nil { + s.Resource.Labels = make(map[string]string) } - s.ACL.Labels[v1alpha1.DeviceLabel] = s.Device.Name + s.Resource.Labels[v1alpha1.DeviceLabel] = s.Device.Name // Ensure the AccessControlList is owned by the Device. - if !controllerutil.HasControllerReference(s.ACL) { - if err := controllerutil.SetOwnerReference(s.Device, s.ACL, r.Scheme, controllerutil.WithBlockOwnerDeletion(true)); err != nil { + if !controllerutil.HasControllerReference(s.Resource) { + if err := controllerutil.SetOwnerReference(s.Device, s.Resource, r.Scheme, controllerutil.WithBlockOwnerDeletion(true)); err != nil { return err } } @@ -222,19 +87,19 @@ func (r *AccessControlListReconciler) reconcile(ctx context.Context, s *aclScope // Ensure the AccessControlList is realized on the provider. err := s.Provider.EnsureACL(ctx, &provider.EnsureACLRequest{ - ACL: s.ACL, + ACL: s.Resource, ProviderConfig: s.ProviderConfig, }) cond := conditions.FromError(err) // As this resource is configuration only, we use the Configured condition as top-level Ready condition. cond.Type = v1alpha1.ReadyCondition - conditions.Set(s.ACL, cond) + conditions.Set(s.Resource, cond) return err } -func (r *AccessControlListReconciler) finalize(ctx context.Context, s *aclScope) (reterr error) { +func (r *AccessControlListReconciler) Finalize(ctx context.Context, s *TypedScope[*v1alpha1.AccessControlList, provider.ACLProvider]) (reterr error) { if err := s.Provider.Connect(ctx, s.Connection); err != nil { return fmt.Errorf("failed to connect to provider: %w", err) } @@ -245,7 +110,7 @@ func (r *AccessControlListReconciler) finalize(ctx context.Context, s *aclScope) }() return s.Provider.DeleteACL(ctx, &provider.DeleteACLRequest{ - Name: s.ACL.Spec.Name, + Name: s.Resource.Spec.Name, ProviderConfig: s.ProviderConfig, }) } diff --git a/internal/controller/controller.go b/internal/controller/controller.go new file mode 100644 index 00000000..21d26acd --- /dev/null +++ b/internal/controller/controller.go @@ -0,0 +1,205 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package controller + +import ( + "context" + "reflect" + + "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kerrors "k8s.io/apimachinery/pkg/util/errors" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/ironcore-dev/network-operator/api/v1alpha1" + "github.com/ironcore-dev/network-operator/internal/conditions" + "github.com/ironcore-dev/network-operator/internal/deviceutil" + "github.com/ironcore-dev/network-operator/internal/provider" +) + +type Object interface { + client.Object + metav1.ObjectMetaAccessor + conditions.Setter + + GetDeviceRef() v1alpha1.LocalObjectReference + GetProviderConfigRef() *v1alpha1.TypedLocalObjectReference + GetStatus() any +} + +// Reconciler is a specialized version of Reconciler that acts on instances of [Object]. +// Depending on whether the object is being created/updated or deleted, either Reconcile +// or Finalize will be called. +type Reconciler[O Object, P provider.Provider] interface { + Reconcile(context.Context, *TypedScope[O, P]) error + Finalize(context.Context, *TypedScope[O, P]) error +} + +type reconciler[O Object, P provider.Provider] struct { + client.Client + + // Provider is the driver that will be used to create & delete the accesscontrollist. + Provider provider.ProviderFunc + + // Reconciler is the actual reconciler that will be called by this generic reconciler. + Reconciler Reconciler[O, P] +} + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// +// For more details about the method shape, read up here: +// - https://ahmet.im/blog/controller-pitfalls/#reconcile-method-shape +func (r *reconciler[O, P]) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { + log := ctrl.LoggerFrom(ctx) + log.Info("Reconciling resource") + + obj := reflect.New(reflect.TypeOf(*new(O)).Elem()).Interface().(O) + if err := r.Get(ctx, req.NamespacedName, obj); err != nil { + if apierrors.IsNotFound(err) { + // If the custom resource is not found then it usually means that it was deleted or not created + // In this way, we will stop the reconciliation + log.Info("Resource not found. Ignoring since object must be deleted") + return ctrl.Result{}, nil + } + // Error reading the object - requeue the request. + log.Error(err, "Failed to get resource") + return ctrl.Result{}, err + } + + prov, ok := r.Provider().(P) + if !ok { + cond := obj.GetConditions() + if meta.SetStatusCondition(&cond, metav1.Condition{ + Type: v1alpha1.ReadyCondition, + Status: metav1.ConditionFalse, + Reason: v1alpha1.NotImplementedReason, + Message: "Provider does not implement provider.Provider", + }) { + obj.SetConditions(cond) + return ctrl.Result{}, r.Status().Update(ctx, obj) + } + return ctrl.Result{}, nil + } + + if !obj.GetDeletionTimestamp().IsZero() { + if controllerutil.ContainsFinalizer(obj, v1alpha1.FinalizerName) { + s, err := r.GetScope(ctx, obj, prov) + if err != nil { + log.Error(err, "Failed to get scope for resource") + return ctrl.Result{}, err + } + if err := r.Reconciler.Finalize(ctx, s); err != nil { + log.Error(err, "Failed to finalize resource") + return ctrl.Result{}, err + } + controllerutil.RemoveFinalizer(obj, v1alpha1.FinalizerName) + if err := r.Update(ctx, obj); err != nil { + log.Error(err, "Failed to remove finalizer from resource") + return ctrl.Result{}, err + } + } + log.Info("Resource is being deleted, skipping reconciliation") + return ctrl.Result{}, nil + } + + // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers + if !controllerutil.ContainsFinalizer(obj, v1alpha1.FinalizerName) { + controllerutil.AddFinalizer(obj, v1alpha1.FinalizerName) + if err := r.Update(ctx, obj); err != nil { + log.Error(err, "Failed to add finalizer to resource") + return ctrl.Result{}, err + } + log.Info("Added finalizer to resource") + return ctrl.Result{}, nil + } + + orig := obj.DeepCopyObject().(O) + if conditions.InitializeConditions(obj, v1alpha1.ReadyCondition) { + log.Info("Initializing status conditions") + return ctrl.Result{}, r.Status().Update(ctx, obj) + } + + // Always attempt to update the metadata/status after reconciliation + defer func() { + if !equality.Semantic.DeepEqual(orig.GetObjectMeta(), obj.GetObjectMeta()) { + if err := r.Patch(ctx, obj, client.MergeFrom(orig)); err != nil { + log.Error(err, "Failed to update resource metadata") + reterr = kerrors.NewAggregate([]error{reterr, err}) + } + return + } + + if !equality.Semantic.DeepEqual(orig.GetStatus(), obj.GetStatus()) { + if err := r.Status().Patch(ctx, obj, client.MergeFrom(orig)); err != nil { + log.Error(err, "Failed to update status") + reterr = kerrors.NewAggregate([]error{reterr, err}) + } + } + }() + + s, err := r.GetScope(ctx, obj, prov) + if err != nil { + log.Error(err, "Failed to get scope for resource") + return ctrl.Result{}, err + } + + if err := r.Reconciler.Reconcile(ctx, s); err != nil { + log.Error(err, "Failed to reconcile resource") + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +func (r *reconciler[O, P]) GetScope(ctx context.Context, obj O, prov P) (*TypedScope[O, P], error) { + device, err := deviceutil.GetDeviceByName(ctx, r, obj.GetNamespace(), obj.GetDeviceRef().Name) + if err != nil { + return nil, err + } + + conn, err := deviceutil.GetDeviceConnection(ctx, r, device) + if err != nil { + return nil, err + } + + var cfg *provider.ProviderConfig + if ref := obj.GetProviderConfigRef(); ref != nil { + cfg, err = provider.GetProviderConfig(ctx, r, obj.GetNamespace(), ref) + if err != nil { + return nil, err + } + } + + return &TypedScope[O, P]{ + Device: device, + Connection: conn, + Resource: obj, + Provider: prov, + ProviderConfig: cfg, + }, nil +} + +// AsReconciler creates a [reconcile.Reconciler] based on the given [Reconciler]. +func AsReconciler[T Object, P provider.Provider](c client.Client, p provider.ProviderFunc, rec Reconciler[T, P]) reconcile.Reconciler { + return &reconciler[T, P]{ + Client: c, + Provider: p, + Reconciler: rec, + } +} + +// TypedScope holds the different objects that are read and used during the reconcile. +type TypedScope[T client.Object, P provider.Provider] struct { + Device *v1alpha1.Device + Connection *deviceutil.Connection + Resource T + Provider P + ProviderConfig *provider.ProviderConfig +}