diff --git a/artifacts/crd/crd.yaml b/artifacts/crd/crd.yaml index 59cb9f9..8f46891 100644 --- a/artifacts/crd/crd.yaml +++ b/artifacts/crd/crd.yaml @@ -2,6 +2,8 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: proxyproviders.proxyprovider.t000-n.de + finalizers: + - proxyprovider.t000-n.de/delete-authentik-proxyprovider spec: group: proxyprovider.t000-n.de versions: diff --git a/controller.go b/controller.go index f781023..feda3f4 100644 --- a/controller.go +++ b/controller.go @@ -19,6 +19,8 @@ package main import ( "context" "fmt" + "net/http" + "slices" "strconv" "time" @@ -55,6 +57,11 @@ const ( FieldManager = controllerAgentName ) +// Finalizers +const ( + DeleteAuthentikProxyProviderFinalizer = "proxyprovider.t000-n.de/delete-authentik-proxyprovider" +) + type Controller struct { kubeclientset kubernetes.Interface proxyProviderClientset clientset.Interface @@ -169,54 +176,97 @@ func (c *Controller) syncHandler(ctx context.Context, objectRef cache.ObjectName } logger.V(4).Info("sync ProxyProvider", "name", pp.Name) - if pp.Status.PK != "" { - // We retrieve the existing PP from the API by slug. - pk, err := strconv.ParseInt(pp.Status.PK, 10, 32) - if err != nil { - return fmt.Errorf("error parsing PK: %v", err) - } - _, _, err = c.authentik.ProvidersApi.ProvidersAllRetrieve(ctx, int32(pk)).Execute() - if err != nil { - return fmt.Errorf("error retrieving existing ProxyProvider: %v", err) - } + if !pp.ObjectMeta.DeletionTimestamp.IsZero() { + logger.Info("Reconciling deletion of ProxyProvider", "name", pp.Name) + return c.reconcileDelete(ctx, pp) + } - // We update the existing PP with the new spec. - proxyProviderRequest := &authentikapi.ProxyProviderRequest{ - Name: pp.Spec.Name, - AuthorizationFlow: pp.Spec.AuthorizationFlow, - InvalidationFlow: pp.Spec.InvalidationFlow, - ExternalHost: pp.Spec.ExternalHost, - Mode: authentikapi.PROXYMODE_FORWARD_SINGLE.Ptr(), - } - resp, r, err := c.authentik.ProvidersApi.ProvidersProxyUpdate(ctx, int32(pk)).ProxyProviderRequest(*proxyProviderRequest).Execute() - if err != nil { - return fmt.Errorf("error when calling `ProvidersAPI.ProvidersProxyUpdate`: %w with response %v", err, r) - } - pp.Status.PK = strconv.Itoa(int(resp.Pk)) - err = c.updateProxyProviderStatus(ctx, pp) - if err != nil { - return fmt.Errorf("error updating ProxyProvider status: %v", err) - } - } else { - proxyProviderRequest := &authentikapi.ProxyProviderRequest{ - Name: pp.Spec.Name, - AuthorizationFlow: pp.Spec.AuthorizationFlow, - InvalidationFlow: pp.Spec.InvalidationFlow, - ExternalHost: pp.Spec.ExternalHost, - Mode: authentikapi.PROXYMODE_FORWARD_SINGLE.Ptr(), - } - resp, r, err := c.authentik.ProvidersApi.ProvidersProxyCreate(ctx).ProxyProviderRequest(*proxyProviderRequest).Execute() - if err != nil { - return fmt.Errorf("error when calling `ProvidersAPI.ProvidersProxyCreate`: %w with response %v", err, r) - } - pp.Status.PK = strconv.Itoa(int(resp.Pk)) - err = c.updateProxyProviderStatus(ctx, pp) - if err != nil { - return fmt.Errorf("error updating ProxyProvider status: %v", err) + if pp.Status.PK == "" { + logger.Info("Reconciling creation of ProxyProvider", "name", pp.Name) + return c.reconcileCreate(ctx, pp) + } + + // Check if all finalizers are present. If not, we add them. Same pattern as above, just needs a helper function to check for presence of a finalizer. + if !slices.Contains(pp.ObjectMeta.Finalizers, DeleteAuthentikProxyProviderFinalizer) { + logger.Info("Ensuring finalizers are present", "name", pp.Name) + return c.ensureFinalizers(ctx, pp) + } + + logger.Info("Reconciling update of ProxyProvider", "name", pp.Name) + return c.reconcileUpdate(ctx, pp) +} + +func (c *Controller) ensureFinalizers(ctx context.Context, pp *v1.ProxyProvider) error { + pp.ObjectMeta.Finalizers = append(pp.ObjectMeta.Finalizers, DeleteAuthentikProxyProviderFinalizer) + return c.updateProxyProvider(ctx, pp) +} + +func (c *Controller) reconcileDelete(ctx context.Context, pp *v1.ProxyProvider) error { + pk, err := strconv.ParseInt(pp.Status.PK, 10, 32) + if err != nil { + return fmt.Errorf("error parsing PK: %v", err) + } + + r, err := c.authentik.ProvidersApi.ProvidersProxyDestroy(ctx, int32(pk)).Execute() + if err != nil { + // This handles an edge-case, where when the ProxyProvider on Authentik has already been deleted, but the finalizer is still present. We just remove the finalizer and return. + if r.StatusCode != http.StatusNotFound { + return fmt.Errorf("error when calling `ProvidersAPI.ProvidersProxyDestroy`: %w with response %v", err, r) } } - return nil + pp.ObjectMeta.Finalizers = slices.Delete(pp.ObjectMeta.Finalizers, slices.Index(pp.ObjectMeta.Finalizers, DeleteAuthentikProxyProviderFinalizer), 1) + return c.updateProxyProvider(ctx, pp) +} + +func (c *Controller) reconcileUpdate(ctx context.Context, pp *v1.ProxyProvider) error { + // We retrieve the existing PP from the API by slug. + pk, err := strconv.ParseInt(pp.Status.PK, 10, 32) + if err != nil { + return fmt.Errorf("error parsing PK: %v", err) + } + _, r, err := c.authentik.ProvidersApi.ProvidersAllRetrieve(ctx, int32(pk)).Execute() + if err != nil && r.StatusCode != http.StatusNotFound { + + return fmt.Errorf("error retrieving existing ProxyProvider: %v with response %v", err, r) + } else if r.StatusCode == http.StatusNotFound { + // This handles an edge-case, where when the PorxyProvider on Authentik has been deleted, e.g. by mistake. We just remove the PK and return. + // During the next reconciliation, the ProxyProvider will be re-created. + pp.Status.PK = "" + } + + proxyProviderRequest := &authentikapi.PatchedProxyProviderRequest{ + Name: &pp.Spec.Name, + AuthorizationFlow: &pp.Spec.AuthorizationFlow, + InvalidationFlow: &pp.Spec.InvalidationFlow, + ExternalHost: &pp.Spec.ExternalHost, + Mode: authentikapi.PROXYMODE_FORWARD_SINGLE.Ptr(), + } + resp, r, err := c.authentik.ProvidersApi.ProvidersProxyPartialUpdate(ctx, int32(pk)).PatchedProxyProviderRequest(*proxyProviderRequest).Execute() + if err != nil { + return fmt.Errorf("error when calling `ProvidersAPI.ProvidersProxyPartialUpdate`: %w with response %v", err, r) + } + + pp.Status.PK = strconv.Itoa(int(resp.Pk)) + + return c.updateProxyProviderStatus(ctx, pp) +} + +func (c *Controller) reconcileCreate(ctx context.Context, pp *v1.ProxyProvider) error { + proxyProviderRequest := &authentikapi.ProxyProviderRequest{ + Name: pp.Spec.Name, + AuthorizationFlow: pp.Spec.AuthorizationFlow, + InvalidationFlow: pp.Spec.InvalidationFlow, + ExternalHost: pp.Spec.ExternalHost, + Mode: authentikapi.PROXYMODE_FORWARD_SINGLE.Ptr(), + } + resp, r, err := c.authentik.ProvidersApi.ProvidersProxyCreate(ctx).ProxyProviderRequest(*proxyProviderRequest).Execute() + if err != nil { + return fmt.Errorf("error when calling `ProvidersAPI.ProvidersProxyCreate`: %w with response %v", err, r) + } + + pp.Status.PK = strconv.Itoa(int(resp.Pk)) + return c.updateProxyProviderStatus(ctx, pp) } func (c *Controller) enqueueProxyProvider(obj interface{}) { @@ -228,26 +278,18 @@ func (c *Controller) enqueueProxyProvider(obj interface{}) { c.workqueue.Add(objectRef) } -func (c *Controller) handleObject(obj interface{}) { - // Optional: resolve Deployment owners back to ProxyProvider and enqueue. - _, ok := obj.(metav1.Object) - if !ok { - tombstone, ok := obj.(cache.DeletedFinalStateUnknown) - if !ok { - utilruntime.HandleError(fmt.Errorf("couldn't get object from tombstone %#v", obj)) - return - } - _, ok = tombstone.Obj.(metav1.Object) - if !ok { - utilruntime.HandleError(fmt.Errorf("tombstone contained object that is not a metav1.Object %#v", obj)) - return - } - } -} - func (c *Controller) updateProxyProviderStatus(ctx context.Context, pp *v1.ProxyProvider) error { ppCopy := pp.DeepCopy() - ppCopy.Status.PK = pp.Status.PK - _, err := c.proxyProviderClientset.ProxyproviderV1().ProxyProviders(pp.Namespace).UpdateStatus(ctx, ppCopy, metav1.UpdateOptions{FieldManager: FieldManager}) + _, err := c.proxyProviderClientset.ProxyproviderV1().ProxyProviders(ppCopy.Namespace).UpdateStatus(ctx, ppCopy, metav1.UpdateOptions{FieldManager: FieldManager}) return err } + +// Update metadata, spec, etc. of the ProxyProvider object. +func (c *Controller) updateProxyProvider(ctx context.Context, pp *v1.ProxyProvider) error { + ppCopy := pp.DeepCopy() + _, err := c.proxyProviderClientset.ProxyproviderV1().ProxyProviders(ppCopy.Namespace).Update(ctx, ppCopy, metav1.UpdateOptions{FieldManager: FieldManager}) + if err != nil { + return fmt.Errorf("error updating ProxyProvider metadata: %v", err) + } + return nil +}