pkg/controller/keyvault/ingress_secret_provider_class.go (138 lines of code) (raw):

// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. package keyvault import ( "context" "errors" "fmt" "strings" "github.com/Azure/aks-app-routing-operator/pkg/config" "github.com/Azure/aks-app-routing-operator/pkg/controller/controllername" "github.com/Azure/aks-app-routing-operator/pkg/controller/metrics" "github.com/Azure/aks-app-routing-operator/pkg/manifests" "github.com/Azure/aks-app-routing-operator/pkg/util" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" netv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" secv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1" ) const nginxAnnotationPrefix = "nginx.ingress.kubernetes.io/" var ingressSecretProviderControllerName = controllername.New("keyvault", "ingress", "secret", "provider") // IngressSecretProviderClassReconciler manages a SecretProviderClass for each ingress resource that // references a Keyvault certificate. The SPC is used to mirror the Keyvault values into a k8s secret // so that it can be used by the ingress controller. type IngressSecretProviderClassReconciler struct { client client.Client events record.EventRecorder config *config.Config ingressManager IngressManager } func NewIngressSecretProviderClassReconciler(manager ctrl.Manager, conf *config.Config, ingressManager IngressManager) error { metrics.InitControllerMetrics(ingressSecretProviderControllerName) if conf.DisableKeyvault { return nil } return ingressSecretProviderControllerName.AddToController( ctrl. NewControllerManagedBy(manager). For(&netv1.Ingress{}). Owns(&secv1.SecretProviderClass{}), manager.GetLogger(), ).Complete(&IngressSecretProviderClassReconciler{ client: manager.GetClient(), events: manager.GetEventRecorderFor("aks-app-routing-operator"), config: conf, ingressManager: ingressManager, }) } func (i *IngressSecretProviderClassReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) { // do metrics defer func() { // placing this call inside a closure allows for result and err to be bound after Reconcile executes // this makes sure they have the proper value // just calling defer metrics.HandleControllerReconcileMetrics(controllerName, result, err) would bind // the values of result and err to their zero values, since they were just instantiated metrics.HandleControllerReconcileMetrics(ingressSecretProviderControllerName, result, retErr) }() logger, err := logr.FromContext(ctx) if err != nil { return ctrl.Result{}, err } logger = ingressSecretProviderControllerName.AddToLogger(logger).WithValues("name", req.Name, "namespace", req.Namespace) logger.Info("getting Ingress") ing := &netv1.Ingress{} err = i.client.Get(ctx, req.NamespacedName, ing) if err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } logger = logger.WithValues("name", ing.Name, "namespace", ing.Namespace, "generation", ing.Generation) spc := &secv1.SecretProviderClass{ TypeMeta: metav1.TypeMeta{ APIVersion: "secrets-store.csi.x-k8s.io/v1", Kind: "SecretProviderClass", }, ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("keyvault-%s", ing.Name), Namespace: ing.Namespace, Labels: manifests.GetTopLevelLabels(), OwnerReferences: []metav1.OwnerReference{{ APIVersion: ing.APIVersion, Controller: util.ToPtr(true), Kind: ing.Kind, Name: ing.Name, UID: ing.UID, }}, }, } logger = logger.WithValues("spc", spc.Name) // Checking if we manage the ingress. All false cases without an error are assumed that we don't manage it var isManaged bool if isManaged, err = i.ingressManager.IsManaging(ing); err != nil { logger.Error(err, fmt.Sprintf("failed while checking if ingress was managed with error: %s.", err.Error())) return ctrl.Result{}, fmt.Errorf("determining if ingress is managed: %w", err) } if isManaged { logManagedIngress(logger, ing) if shouldDeploySpc(ing) { spcConf := spcConfig{ ClientId: i.config.MSIClientID, TenantId: i.config.TenantID, KeyvaultCertUri: ing.Annotations["kubernetes.azure.com/tls-cert-keyvault-uri"], Name: certSecretName(ing.Name), Cloud: i.config.Cloud, } if err = buildSPC(spc, spcConf); err != nil { var userErr util.UserError if errors.As(err, &userErr) { logger.Info(fmt.Sprintf("failed to build secret provider class for ingress with error: %s. sending warning event", userErr.Error())) i.events.Eventf(ing, corev1.EventTypeWarning, "InvalidInput", "error while processing Keyvault reference: %s", userErr.UserError()) return ctrl.Result{}, nil } logger.Error(err, fmt.Sprintf("failed to build secret provider class for ingress with error: %s.", err.Error())) return ctrl.Result{}, err } logger.Info("reconciling secret provider class for ingress") if err = util.Upsert(ctx, i.client, spc); err != nil { i.events.Eventf(ing, corev1.EventTypeWarning, "FailedUpdateOrCreateSPC", "error while creating or updating SecretProviderClass needed to pull Keyvault reference: %s", err.Error()) logger.Error(err, fmt.Sprintf("failed to upsert secret provider class for ingress with error: %s.", err.Error())) } return ctrl.Result{}, err } } logger.Info("cleaning unused managed spc for ingress") logger.Info("getting secret provider class for ingress") toCleanSPC := &secv1.SecretProviderClass{} if err = i.client.Get(ctx, client.ObjectKeyFromObject(spc), toCleanSPC); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } if manifests.HasTopLevelLabels(toCleanSPC.Labels) { logger.Info("removing secret provider class for ingress") err = i.client.Delete(ctx, toCleanSPC) return ctrl.Result{}, client.IgnoreNotFound(err) } return result, nil } func logManagedIngress(l logr.Logger, ing *netv1.Ingress) { if ing == nil { return } l = l.WithValues("name", ing.Name, "namespace", ing.Namespace, "generation", ing.Generation, "uid", ing.UID) nginxAnnotations := util.FilterMap(ing.Annotations, isNginxAnnotation) l.Info("Ingress using managed Ingress Controller", "nginxAnnotations", nginxAnnotations) } func isNginxAnnotation(key, _ string) bool { cleaned := strings.TrimSpace(key) return strings.HasPrefix(cleaned, nginxAnnotationPrefix) }