From 93ca85f7afaeed6c745991b5368f807d916f8689 Mon Sep 17 00:00:00 2001 From: shreddedbacon Date: Mon, 8 Dec 2025 19:22:10 +1100 Subject: [PATCH 1/2] 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), From 5c2c37d05d8deb69886e0623df14965eb3cd8605 Mon Sep 17 00:00:00 2001 From: shreddedbacon Date: Fri, 27 Feb 2026 10:06:39 +1100 Subject: [PATCH 2/2] feat: limited haproxy support --- internal/handlers/unidler/handler.go | 56 +++++++++++++++++++++++++++- internal/handlers/unidler/unidler.go | 2 + resources/html/unidle.html | 2 +- 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/internal/handlers/unidler/handler.go b/internal/handlers/unidler/handler.go index cdc9fac..97d8011 100644 --- a/internal/handlers/unidler/handler.go +++ b/internal/handlers/unidler/handler.go @@ -15,6 +15,8 @@ import ( "github.com/uselagoon/aergia-controller/internal/handlers/metrics" corev1 "k8s.io/api/core/v1" networkv1 "k8s.io/api/networking/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" "k8s.io/apimachinery/pkg/types" ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -57,8 +59,36 @@ func (h *Unidler) ingressHandler(path string) func(http.ResponseWriter, *http.Re } w.WriteHeader(code) ns := r.Header.Get(Namespace) + originalURI := r.Header.Get(OriginalURI) + serviceName := r.Header.Get(ServiceName) ingressName := r.Header.Get(IngressName) hostname := "" + + // haproxy requests will set query param `unidle=true` + unidleReq := r.URL.Query().Get("unidle") + if unidleReq != "" { + opLog.Info(fmt.Sprintf("UnidleReq Param: %s", unidleReq)) + // and haproxy requests include the `Referer` header for which url was requested by the client + urlParam := r.Header.Get(Referer) + if urlParam != "" { + // if unidle and referrer are set + opLog.Info(fmt.Sprintf("Referer: %s", urlParam)) + url, err := url.Parse(urlParam) + if err != nil { + opLog.Info(fmt.Sprintf("URL err: %v", err)) + } + originalURI = urlParam + hostname = url.Hostname() + // look for an idled ingress that matches this hostname + i, svcName, err := h.getIngressByHostname(ctx, hostname) + if err == nil { + ns = i.Namespace + serviceName = svcName + } + } + } + + // traefik requests will set the `namespace` and `url` query params nsParam := r.URL.Query().Get("namespace") if nsParam != "" { opLog.Info(fmt.Sprintf("Namespace Param: %s", nsParam)) @@ -165,10 +195,10 @@ func (h *Unidler) ingressHandler(path string) func(http.ResponseWriter, *http.Re FormatHeader: r.Header.Get(FormatHeader), CodeHeader: r.Header.Get(CodeHeader), ContentType: r.Header.Get(ContentType), - OriginalURI: r.Header.Get(OriginalURI), + OriginalURI: originalURI, Namespace: ns, IngressName: ingress.Name, - ServiceName: r.Header.Get(ServiceName), + ServiceName: serviceName, ServicePort: r.Header.Get(ServicePort), RequestID: r.Header.Get(RequestID), RefreshInterval: h.RefreshInterval, @@ -247,3 +277,25 @@ func (h *Unidler) setMetrics(r *http.Request, start time.Time) { metrics.RequestCount.WithLabelValues(proto).Inc() metrics.RequestDuration.WithLabelValues(proto).Observe(duration) } + +func (h *Unidler) getIngressByHostname(ctx context.Context, targetHost string) (*networkv1.Ingress, string, error) { + ingressList := &networkv1.IngressList{} + labelRequirements, _ := labels.NewRequirement("idling.amazee.io/idled", selection.Equals, []string{"true"}) + listOption := (&ctrlClient.ListOptions{}).ApplyOptions([]ctrlClient.ListOption{ + ctrlClient.MatchingLabelsSelector{ + Selector: labels.NewSelector().Add(*labelRequirements), + }, + }) + err := h.Client.List(ctx, ingressList, listOption) + if err != nil { + return nil, "", err + } + for _, ingress := range ingressList.Items { + for _, rule := range ingress.Spec.Rules { + if rule.Host == targetHost { + return &ingress, rule.HTTP.Paths[0].Backend.Service.Name, nil + } + } + } + return nil, "", fmt.Errorf("ingress with host %s not found", targetHost) +} diff --git a/internal/handlers/unidler/unidler.go b/internal/handlers/unidler/unidler.go index 30c83e1..d3d281f 100644 --- a/internal/handlers/unidler/unidler.go +++ b/internal/handlers/unidler/unidler.go @@ -87,6 +87,8 @@ const ( AergiaHeader = "X-Aergia" // CacheControl name of the header that defines the cache control config CacheControl = "Cache-Control" + // Referrer name of the header that defines the referrer + Referer = "Referer" // ErrFilesPathVar is the name of the environment variable indicating // the location on disk of files served by the handler. ErrFilesPathVar = "ERROR_FILES_PATH" diff --git a/resources/html/unidle.html b/resources/html/unidle.html index 6a45952..61fa828 100644 --- a/resources/html/unidle.html +++ b/resources/html/unidle.html @@ -4,7 +4,7 @@ Preparing your environment | Lagoon - +