pkg/controller/keyvault/placeholder_pod.go (313 lines of code) (raw):

// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. package keyvault import ( "context" "errors" "fmt" "path" "strconv" "github.com/Azure/aks-app-routing-operator/api/v1alpha1" "github.com/Azure/aks-app-routing-operator/pkg/util" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/go-logr/logr" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" netv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" secv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1" "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" ) var placeholderPodControllerName = controllername.New("keyvault", "placeholder", "pod") // PlaceholderPodController manages a single-replica deployment of no-op pods that mount the // Keyvault secrets referenced by each secret provider class managed by IngressSecretProviderClassReconciler. // // This is necessitated by the Keyvault CSI implementation, which requires at least one mount // in order to start mirroring the Keyvault values into corresponding Kubernetes secret(s). type PlaceholderPodController struct { client client.Client events record.EventRecorder config *config.Config ingressManager IngressManager } func NewPlaceholderPodController(manager ctrl.Manager, conf *config.Config, ingressManager IngressManager) error { metrics.InitControllerMetrics(placeholderPodControllerName) if conf.DisableKeyvault { return nil } return placeholderPodControllerName.AddToController( ctrl. NewControllerManagedBy(manager). For(&secv1.SecretProviderClass{}), manager.GetLogger(), ).Complete(&PlaceholderPodController{ client: manager.GetClient(), config: conf, ingressManager: ingressManager, events: manager.GetEventRecorderFor("aks-app-routing-operator"), }) } func (p *PlaceholderPodController) Reconcile(ctx context.Context, req ctrl.Request) (res 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(placeholderPodControllerName, res, retErr) }() logger, err := logr.FromContext(ctx) if err != nil { return ctrl.Result{}, fmt.Errorf("creating logger: %w", err) } logger = placeholderPodControllerName.AddToLogger(logger).WithValues("namespace", req.Namespace, "name", req.Name) logger.Info("getting secret provider class") spc := &secv1.SecretProviderClass{} err = p.client.Get(ctx, req.NamespacedName, spc) if err != nil { if client.IgnoreNotFound(err) != nil { logger.Error(err, "failed to fetch SPC: %s", err.Error()) return ctrl.Result{}, fmt.Errorf("fetching SPC: %w", err) } return ctrl.Result{}, nil } logger = logger.WithValues("name", spc.Name, "namespace", spc.Namespace, "generation", spc.Generation) dep := &appsv1.Deployment{ TypeMeta: metav1.TypeMeta{ APIVersion: "apps/v1", Kind: "Deployment", }, ObjectMeta: metav1.ObjectMeta{ Name: spc.Name, Namespace: spc.Namespace, Labels: spc.Labels, OwnerReferences: []metav1.OwnerReference{{ APIVersion: spc.APIVersion, Controller: util.ToPtr(true), Kind: spc.Kind, Name: spc.Name, UID: spc.UID, }}, }, } logger = logger.WithValues("deployment", dep.Name) return p.reconcileObjectDeployment(dep, spc, req, ctx, logger) } func (p *PlaceholderPodController) reconcileObjectDeployment(dep *appsv1.Deployment, spc *secv1.SecretProviderClass, req ctrl.Request, ctx context.Context, logger logr.Logger) (ctrl.Result, error) { var ( err error obj client.Object serviceAccount string ) result := ctrl.Result{} switch { case util.FindOwnerKind(spc.OwnerReferences, "NginxIngressController") != "": obj = &v1alpha1.NginxIngressController{} obj.SetName(util.FindOwnerKind(spc.OwnerReferences, "NginxIngressController")) logger.Info(fmt.Sprint("getting owner NginxIngressController")) case util.FindOwnerKind(spc.OwnerReferences, "Ingress") != "": obj = &netv1.Ingress{} obj.SetName(util.FindOwnerKind(spc.OwnerReferences, "Ingress")) obj.SetNamespace(req.Namespace) logger.Info(fmt.Sprint("getting owner Ingress")) case util.FindOwnerKind(spc.OwnerReferences, "Gateway") != "": obj = &gatewayv1.Gateway{} gwName := util.FindOwnerKind(spc.OwnerReferences, "Gateway") obj.SetName(gwName) obj.SetNamespace(req.Namespace) logger.Info(fmt.Sprintf("getting owner Gateway resource %s", gwName)) default: logger.Info("owner type not found") return result, nil } if obj.GetName() != "" { if err = p.client.Get(ctx, client.ObjectKeyFromObject(obj), obj); err != nil { return result, client.IgnoreNotFound(err) } } cleanPod, err := p.placeholderPodCleanCheck(spc, obj) if err != nil { return result, err } if cleanPod { logger.Info("attempting to clean unused placeholder pod deployment") logger.Info("getting placeholder deployment") toCleanDeployment := &appsv1.Deployment{} if err = p.client.Get(ctx, client.ObjectKeyFromObject(dep), toCleanDeployment); err != nil { return result, client.IgnoreNotFound(err) } if manifests.HasTopLevelLabels(toCleanDeployment.Labels) { logger.Info("deleting placeholder deployment") err = p.client.Delete(ctx, toCleanDeployment) return result, client.IgnoreNotFound(err) } logger.Info("deployment found but it's not managed by us, skipping cleaning") return result, nil } // Verify ServiceAccount exists (if Gateway) serviceAccount, err = p.verifyServiceAccount(ctx, spc, obj, logger) if err != nil { var userErr util.UserError if errors.As(err, &userErr) { logger.Info("user error while verifying if service account exists: %s", userErr.Err) p.events.Eventf(obj, corev1.EventTypeWarning, "InvalidInput", userErr.UserMessage) return result, nil } logger.Error(err, "verifying ServiceAccount for placeholder pod") return result, fmt.Errorf("verifying service account for placeholder pod: %s", err.Error()) } // Manage a deployment resource logger.Info("reconciling placeholder deployment for secret provider class") if err = p.buildDeployment(ctx, dep, spc, obj); err != nil { err = fmt.Errorf("building deployment: %w", err) p.events.Eventf(obj, corev1.EventTypeWarning, "FailedUpdateOrCreatePlaceholderPodDeployment", "error while building placeholder pod Deployment needed to pull Keyvault reference: %s", err.Error()) logger.Error(err, "failed to build placeholder deployment") return result, err } if serviceAccount != "" { dep.Spec.Template.Spec.AutomountServiceAccountToken = to.Ptr(true) dep.Spec.Template.Spec.ServiceAccountName = serviceAccount } if err = util.Upsert(ctx, p.client, dep); err != nil { p.events.Eventf(obj, corev1.EventTypeWarning, "FailedUpdateOrCreatePlaceholderPodDeployment", "error while creating or updating placeholder pod Deployment needed to pull Keyvault reference: %s", err.Error()) logger.Error(err, "failed to upsert placeholder deployment") return result, err } return result, nil } func (p *PlaceholderPodController) placeholderPodCleanCheck(spc *secv1.SecretProviderClass, obj client.Object) (bool, error) { switch t := obj.(type) { case *v1alpha1.NginxIngressController: if t.Spec.DefaultSSLCertificate == nil || t.Spec.DefaultSSLCertificate.KeyVaultURI == nil { return true, nil } case *netv1.Ingress: managed, err := p.ingressManager.IsManaging(t) if err != nil { return false, fmt.Errorf("determining if ingress is managed: %w", err) } if t.Name == "" || t.Spec.IngressClassName == nil || !managed { return true, nil } case *gatewayv1.Gateway: if !shouldReconcileGateway(t) { return true, nil } for _, listener := range t.Spec.Listeners { if spc.Name != generateGwListenerCertName(t.Name, listener.Name) { continue } return !listenerIsKvEnabled(listener), nil } // couldn't find the listener the pod belongs to so return true return true, nil } return false, nil } // getCurrentDeployment returns the current deployment for the given name or nil if it does not exist. nil, nil is returned if the deployment is not found func (p *PlaceholderPodController) getCurrentDeployment(ctx context.Context, name types.NamespacedName) (*appsv1.Deployment, error) { dep := &appsv1.Deployment{} err := p.client.Get(ctx, name, dep) if err != nil { return nil, client.IgnoreNotFound(err) } return dep, nil } func (p *PlaceholderPodController) buildDeployment(ctx context.Context, dep *appsv1.Deployment, spc *secv1.SecretProviderClass, obj client.Object) error { old, err := p.getCurrentDeployment(ctx, client.ObjectKeyFromObject(dep)) if err != nil { return fmt.Errorf("getting current deployment: %w", err) } labels := map[string]string{"app": spc.Name} if old != nil { // we need to ensure that immutable fields are not changed labels = old.Spec.Selector.MatchLabels } var ownerAnnotation string switch obj.(type) { case *v1alpha1.NginxIngressController: ownerAnnotation = "kubernetes.azure.com/nginx-ingress-controller-owner" case *netv1.Ingress: ownerAnnotation = "kubernetes.azure.com/ingress-owner" case *gatewayv1.Gateway: ownerAnnotation = "kubernetes.azure.com/gateway-owner" default: return fmt.Errorf("failed to build deployment: object type not ingress, nginxingresscontroller, or gateway") } dep.Spec = appsv1.DeploymentSpec{ Replicas: util.Int32Ptr(1), RevisionHistoryLimit: util.Int32Ptr(2), Selector: &metav1.LabelSelector{MatchLabels: labels}, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: labels, Annotations: map[string]string{ "kubernetes.azure.com/observed-generation": strconv.FormatInt(spc.Generation, 10), "kubernetes.azure.com/purpose": "hold CSI mount to enable keyvault-to-k8s secret mirroring", ownerAnnotation: obj.GetName(), "openservicemesh.io/sidecar-injection": "disabled", }, }, Spec: *manifests.WithPreferSystemNodes(&corev1.PodSpec{ AutomountServiceAccountToken: util.ToPtr(false), Containers: []corev1.Container{{ Name: "placeholder", Image: path.Join(p.config.Registry, "/oss/kubernetes/pause:3.10"), VolumeMounts: []corev1.VolumeMount{{ Name: "secrets", MountPath: "/mnt/secrets", ReadOnly: true, }}, Resources: corev1.ResourceRequirements{ Limits: corev1.ResourceList{ corev1.ResourceCPU: resource.MustParse("20m"), corev1.ResourceMemory: resource.MustParse("24Mi"), }, }, SecurityContext: &corev1.SecurityContext{ Privileged: util.ToPtr(false), AllowPrivilegeEscalation: util.ToPtr(false), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, RunAsNonRoot: util.ToPtr(true), RunAsUser: util.Int64Ptr(65535), RunAsGroup: util.Int64Ptr(65535), ReadOnlyRootFilesystem: util.ToPtr(true), SeccompProfile: &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeRuntimeDefault, }, }, }}, Volumes: []corev1.Volume{{ Name: "secrets", VolumeSource: corev1.VolumeSource{ CSI: &corev1.CSIVolumeSource{ Driver: "secrets-store.csi.k8s.io", ReadOnly: util.ToPtr(true), VolumeAttributes: map[string]string{"secretProviderClass": spc.Name}, }, }, }}, }), }, } return nil } // verifyServiceAccount ensures that the ServiceAccount used to create the placeholder pod exists func (p *PlaceholderPodController) verifyServiceAccount(ctx context.Context, spc *secv1.SecretProviderClass, obj client.Object, logger logr.Logger) (string, error) { var serviceAccount string switch t := obj.(type) { case *gatewayv1.Gateway: logger.Info("verifying service account referenced by listener exists") for _, listener := range t.Spec.Listeners { if spc.Name != generateGwListenerCertName(t.Name, listener.Name) { continue } if listener.TLS != nil && listener.TLS.Options != nil { serviceAccount = string(listener.TLS.Options[serviceAccountTLSOption]) break } } if serviceAccount == "" { err := fmt.Errorf("failed to locate listener for SPC %s on user's gateway resource", spc.Name) return "", util.NewUserError(err, fmt.Sprintf("gateway listener for spc %s doesn't exist or doesn't contain required TLS options", spc.Name)) } _, err := util.GetServiceAccountAndVerifyWorkloadIdentity(ctx, p.client, serviceAccount, spc.Namespace) if err != nil { return "", err } return serviceAccount, nil } return "", nil }