pkg/controller/service/ingress_reconciler.go (119 lines of code) (raw):

// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. package service import ( "context" "fmt" "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" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) var ( ingressControllerName = controllername.New("service", "ingress", "reconciler") ) // NginxIngressReconciler manages an opinionated ingress resource for services that define certain annotations. // The resulting ingress uses Keyvault for TLS, never exposes insecure (plain http) routes, and uses OSM for upstream mTLS. // If those integrations aren't enabled, it won't work correctly. // // Annotations: // - kubernetes.azure.com/ingress-host: host of the ingress resource // - kubernetes.azure.com/tls-cert-keyvault-uri: URI of the Keyvault certificate to present // - kubernetes.azure.com/service-account-name: name of the service account used by upstream pods (defaults to "default") // - kubernetes.azure.com/insecure-disable-osm: don't use OSM integration. Connections between ingreses controller and app will be insecure. // // This functionality allows easy adoption of good ingress practices while providing an exit strategy. // Users can remove the annotations and take ownership of the generated resources at any time. type NginxIngressReconciler struct { client client.Client ingConfig *manifests.NginxIngressConfig } func NewNginxIngressReconciler(manager ctrl.Manager, ingConfig *manifests.NginxIngressConfig) error { metrics.InitControllerMetrics(ingressControllerName) return ingressControllerName.AddToController( ctrl. NewControllerManagedBy(manager). For(&corev1.Service{}), manager.GetLogger(), ).Complete(&NginxIngressReconciler{client: manager.GetClient(), ingConfig: ingConfig}) } func (i *NginxIngressReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { var err error result := ctrl.Result{} // 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(ingressControllerName, result, err) }() logger, err := logr.FromContext(ctx) if err != nil { return result, err } logger = ingressControllerName.AddToLogger(logger) logger.Info("getting service", "name", req.Name, "namespace", req.Namespace) svc := &corev1.Service{} err = i.client.Get(ctx, req.NamespacedName, svc) if errors.IsNotFound(err) { return result, nil } if err != nil { return result, err } logger = logger.WithValues("name", svc.Name, "namespace", svc.Namespace, "generation", svc.Generation) if svc.Annotations == nil || svc.Annotations["kubernetes.azure.com/ingress-host"] == "" || svc.Annotations["kubernetes.azure.com/tls-cert-keyvault-uri"] == "" || len(svc.Spec.Ports) == 0 { // Give users a migration path away from managed ingress, etc. resources by not cleaning them up if annotations are removed. // Users can remove the annotations, remove the owner references from managed resources, and take ownership of them. logger.Info("skipping service without required annotations or ports") return result, nil } serviceAccount := "default" if sa := svc.Annotations["kubernetes.azure.com/service-account-name"]; sa != "" { logger.Info("using annotation defined service account", "name", sa) serviceAccount = sa } pt := netv1.PathTypePrefix ing := &netv1.Ingress{ TypeMeta: metav1.TypeMeta{ Kind: "Ingress", APIVersion: "networking.k8s.io/v1", }, ObjectMeta: metav1.ObjectMeta{ Name: svc.Name, Namespace: svc.Namespace, OwnerReferences: []metav1.OwnerReference{{ APIVersion: svc.APIVersion, Controller: util.ToPtr(true), Kind: svc.Kind, Name: svc.Name, UID: svc.UID, }}, Annotations: map[string]string{ "kubernetes.azure.com/tls-cert-keyvault-uri": svc.Annotations["kubernetes.azure.com/tls-cert-keyvault-uri"], }, }, Spec: netv1.IngressSpec{ IngressClassName: util.ToPtr(i.ingConfig.IcName), Rules: []netv1.IngressRule{{ Host: svc.Annotations["kubernetes.azure.com/ingress-host"], IngressRuleValue: netv1.IngressRuleValue{ HTTP: &netv1.HTTPIngressRuleValue{ Paths: []netv1.HTTPIngressPath{{ Path: "/", PathType: &pt, Backend: netv1.IngressBackend{ Service: &netv1.IngressServiceBackend{ Name: svc.Name, Port: netv1.ServiceBackendPort{Number: svc.Spec.Ports[0].TargetPort.IntVal}, }, }, }}, }, }, }}, TLS: []netv1.IngressTLS{{ Hosts: []string{svc.Annotations["kubernetes.azure.com/ingress-host"]}, SecretName: fmt.Sprintf("keyvault-%s", svc.Name), }}, }, } if svc.Annotations["kubernetes.azure.com/insecure-disable-osm"] == "" { logger.Info("adding OSM annotations") ing.Annotations["kubernetes.azure.com/use-osm-mtls"] = "true" ing.Annotations["nginx.ingress.kubernetes.io/backend-protocol"] = "HTTPS" ing.Annotations["nginx.ingress.kubernetes.io/configuration-snippet"] = fmt.Sprintf("\nproxy_ssl_name \"%s.%s.cluster.local\";", serviceAccount, svc.Namespace) ing.Annotations["nginx.ingress.kubernetes.io/proxy-ssl-secret"] = "kube-system/osm-ingress-client-cert" ing.Annotations["nginx.ingress.kubernetes.io/proxy-ssl-verify"] = "on" } logger.Info("reconciling ingress for service") err = util.Upsert(ctx, i.client, ing) return result, err }