From 93ca85f7afaeed6c745991b5368f807d916f8689 Mon Sep 17 00:00:00 2001 From: shreddedbacon Date: Mon, 8 Dec 2025 19:22:10 +1100 Subject: [PATCH] feat: initial traefik support --- internal/handlers/idler/helpers.go | 16 +++++ internal/handlers/idler/service-kubernetes.go | 15 ++++- internal/handlers/unidler/checks.go | 61 ++++++++++++------- internal/handlers/unidler/handler.go | 57 ++++++++++++++--- 4 files changed, 114 insertions(+), 35 deletions(-) diff --git a/internal/handlers/idler/helpers.go b/internal/handlers/idler/helpers.go index 04fc595..09cd264 100644 --- a/internal/handlers/idler/helpers.go +++ b/internal/handlers/idler/helpers.go @@ -1,6 +1,8 @@ package idler import ( + "strings" + "k8s.io/apimachinery/pkg/labels" ) @@ -17,3 +19,17 @@ func generateLabelRequirements(selectors []idlerSelector) []labels.Requirement { } return labelRequirements } + +func addStatusCode(codes string, code string) *string { + if codes == "" { + return &code + } + parts := strings.Split(codes, ",") + for _, c := range parts { + if c == code { + return &codes + } + } + newCodes := codes + "," + code + return &newCodes +} diff --git a/internal/handlers/idler/service-kubernetes.go b/internal/handlers/idler/service-kubernetes.go index e492738..693e6fa 100644 --- a/internal/handlers/idler/service-kubernetes.go +++ b/internal/handlers/idler/service-kubernetes.go @@ -124,6 +124,7 @@ func (h *Idler) KubernetesServiceIdler(ctx context.Context, opLog logr.Logger, n ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // get the number of requests to any ingress in the exported namespace by status code + // @TODO: traefik_service_requests_total{exported_service="test-develop-nginx-http@kubernetes"} promQuery := fmt.Sprintf( `round(sum(increase(nginx_ingress_controller_requests{exported_namespace="%s",status=~"2[0-9x]{2}"}[%s])) by (status))`, namespace.Name, @@ -241,14 +242,24 @@ func (h *Idler) patchIngress(ctx context.Context, opLog logr.Logger, namespace c for _, ingress := range ingressList.Items { if !h.DryRun { ingressCopy := ingress.DeepCopy() + var ingressCodes, traefikMiddlewares *string + ingressValue, ok := ingress.Annotations["nginx.ingress.kubernetes.io/custom-http-errors"] + if ok { + ingressCodes = addStatusCode(ingressValue, "503") + } + traefikValue, ok := ingress.Annotations["traefik.ingress.kubernetes.io/router.middlewares"] + if ok { + traefikMiddlewares = addStatusCode(traefikValue, fmt.Sprintf("%s-aergia@kubernetescrd", ingress.Namespace)) + } mergePatch, _ := json.Marshal(map[string]interface{}{ "metadata": map[string]interface{}{ "labels": map[string]string{ "idling.amazee.io/idled": "true", }, - "annotations": map[string]string{ + "annotations": map[string]interface{}{ // add the custom-http-errors annotation so that the unidler knows to handle this ingress - "nginx.ingress.kubernetes.io/custom-http-errors": "503", + "nginx.ingress.kubernetes.io/custom-http-errors": ingressCodes, + "traefik.ingress.kubernetes.io/router.middlewares": traefikMiddlewares, }, }, }) diff --git a/internal/handlers/unidler/checks.go b/internal/handlers/unidler/checks.go index d278479..4c9f739 100644 --- a/internal/handlers/unidler/checks.go +++ b/internal/handlers/unidler/checks.go @@ -65,39 +65,54 @@ func (h *Unidler) removeCodeFromIngress(ctx context.Context, ns string, opLog lo }) ingresses := &networkv1.IngressList{} if err := h.Client.List(ctx, ingresses, listOption); err != nil { - opLog.Info(fmt.Sprintf("Unable to get any deployments - %s", ns)) + opLog.Info(fmt.Sprintf("Unable to get any ingress - %s", ns)) return } for _, ingress := range ingresses.Items { // if the nginx.ingress.kubernetes.io/custom-http-errors annotation is set // then strip out the 503 error code that is there so that // users will see their application errors rather than the loading page - if value, ok := ingress.Annotations["nginx.ingress.kubernetes.io/custom-http-errors"]; ok { - newVals := removeStatusCode(value, "503") + var ingressCodes, traefikMiddlewares *string + patch := false + ingressValue, ok := ingress.Annotations["nginx.ingress.kubernetes.io/custom-http-errors"] + if ok { + ingressCodes = removeStatusCode(ingressValue, "503") + patch = true + } + traefikValue, ok := ingress.Annotations["traefik.ingress.kubernetes.io/router.middlewares"] + if ok { + traefikMiddlewares = removeStatusCode(traefikValue, fmt.Sprintf("%s-aergia@kubernetescrd", ingress.Namespace)) + patch = true + } + if patch { // if the 503 code was removed from the annotation // then patch it - if newVals == nil || *newVals != value { - mergePatch, _ := json.Marshal(map[string]interface{}{ - "metadata": map[string]interface{}{ - "labels": map[string]interface{}{ - "idling.amazee.io/idled": "false", - }, - "annotations": map[string]interface{}{ - "nginx.ingress.kubernetes.io/custom-http-errors": newVals, - "idling.amazee.io/idled-at": nil, - }, + annotations := map[string]interface{}{ + "idling.amazee.io/idled-at": nil, + } + if ingressCodes == nil || *ingressCodes != ingressValue { + annotations["nginx.ingress.kubernetes.io/custom-http-errors"] = ingressCodes + } + if traefikMiddlewares == nil || *traefikMiddlewares != traefikValue { + annotations["traefik.ingress.kubernetes.io/router.middlewares"] = traefikMiddlewares + } + mergePatch, _ := json.Marshal(map[string]interface{}{ + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "idling.amazee.io/idled": "false", }, - }) - patchIngress := ingress.DeepCopy() - if err := h.Client.Patch(ctx, patchIngress, ctrlClient.RawPatch(types.MergePatchType, mergePatch)); err != nil { - // log it but try and patch the rest of the ingressses anyway (some is better than none?) - opLog.Info(fmt.Sprintf("Error patching custom-http-errors on ingress %s - %s", ingress.Name, ns)) + "annotations": annotations, + }, + }) + patchIngress := ingress.DeepCopy() + if err := h.Client.Patch(ctx, patchIngress, ctrlClient.RawPatch(types.MergePatchType, mergePatch)); err != nil { + // log it but try and patch the rest of the ingressses anyway (some is better than none?) + opLog.Info(fmt.Sprintf("Error patching custom-http-errors on ingress %s - %s", ingress.Name, ns)) + } else { + if ingressCodes == nil { + opLog.Info(fmt.Sprintf("Ingress %s custom-http-errors annotation removed - %s", ingress.Name, ns)) } else { - if newVals == nil { - opLog.Info(fmt.Sprintf("Ingress %s custom-http-errors annotation removed - %s", ingress.Name, ns)) - } else { - opLog.Info(fmt.Sprintf("Ingress %s custom-http-errors annotation patched with %s - %s", ingress.Name, *newVals, ns)) - } + opLog.Info(fmt.Sprintf("Ingress %s custom-http-errors annotation patched with %s - %s", ingress.Name, *ingressCodes, ns)) } } } diff --git a/internal/handlers/unidler/handler.go b/internal/handlers/unidler/handler.go index cc600c8..cdc9fac 100644 --- a/internal/handlers/unidler/handler.go +++ b/internal/handlers/unidler/handler.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "net/url" "os" "strconv" "strings" @@ -15,6 +16,7 @@ import ( corev1 "k8s.io/api/core/v1" networkv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/types" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" ) func (h *Unidler) ingressHandler(path string) func(http.ResponseWriter, *http.Request) { @@ -56,6 +58,21 @@ func (h *Unidler) ingressHandler(path string) func(http.ResponseWriter, *http.Re w.WriteHeader(code) ns := r.Header.Get(Namespace) ingressName := r.Header.Get(IngressName) + hostname := "" + nsParam := r.URL.Query().Get("namespace") + if nsParam != "" { + opLog.Info(fmt.Sprintf("Namespace Param: %s", nsParam)) + ns = nsParam + } + urlParam := r.URL.Query().Get("url") + if urlParam != "" { + url, err := url.Parse(urlParam) + if err != nil { + opLog.Info(fmt.Sprintf("URL Param err: %v", err)) + } + opLog.Info(fmt.Sprintf("URL Param: %s", urlParam)) + hostname = url.Hostname() + } // check if the namespace exists so we know this is somewhat legitimate request if ns != "" { namespace := &corev1.Namespace{} @@ -66,14 +83,34 @@ func (h *Unidler) ingressHandler(path string) func(http.ResponseWriter, *http.Re return } ingress := &networkv1.Ingress{} - if err := h.Client.Get(ctx, types.NamespacedName{ - Namespace: ns, - Name: ingressName, - }, ingress); err != nil { - opLog.Info(fmt.Sprintf("Unable to get the ingress %s in %s", ingressName, ns)) - h.genericError(w, r, opLog, format, path, 400) - h.setMetrics(r, start) - return + if ingressName != "" { + if err := h.Client.Get(ctx, types.NamespacedName{ + Namespace: ns, + Name: ingressName, + }, ingress); err != nil { + opLog.Info(fmt.Sprintf("Unable to get the ingress %s in %s", ingressName, ns)) + h.genericError(w, r, opLog, format, path, 400) + h.setMetrics(r, start) + return + } + } else { + listOption := (&ctrlClient.ListOptions{}).ApplyOptions([]ctrlClient.ListOption{ + ctrlClient.InNamespace(ns), + }) + ingresses := &networkv1.IngressList{} + if err := h.Client.List(ctx, ingresses, listOption); err != nil { + opLog.Info(fmt.Sprintf("Unable to get any ingress - %s", ns)) + return + } + for _, ingressss := range ingresses.Items { + for _, rule := range ingressss.Spec.Rules { + for _, host := range rule.Host { + if string(host) == hostname { + ingress = ingressss.DeepCopy() + } + } + } + } } // if hmac verification is enabled, perform the verification of the request signedNamespace, verfied := h.verifyRequest(r, namespace, ingress) @@ -129,8 +166,8 @@ func (h *Unidler) ingressHandler(path string) func(http.ResponseWriter, *http.Re CodeHeader: r.Header.Get(CodeHeader), ContentType: r.Header.Get(ContentType), OriginalURI: r.Header.Get(OriginalURI), - Namespace: r.Header.Get(Namespace), - IngressName: r.Header.Get(IngressName), + Namespace: ns, + IngressName: ingress.Name, ServiceName: r.Header.Get(ServiceName), ServicePort: r.Header.Get(ServicePort), RequestID: r.Header.Get(RequestID),