Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions internal/handlers/idler/helpers.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package idler

import (
"strings"

"k8s.io/apimachinery/pkg/labels"
)

Expand All @@ -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
}
15 changes: 13 additions & 2 deletions internal/handlers/idler/service-kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
},
},
})
Expand Down
61 changes: 38 additions & 23 deletions internal/handlers/unidler/checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
}
Expand Down
113 changes: 101 additions & 12 deletions internal/handlers/unidler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/http"
"net/url"
"os"
"strconv"
"strings"
Expand All @@ -14,7 +15,10 @@ 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"
)

func (h *Unidler) ingressHandler(path string) func(http.ResponseWriter, *http.Request) {
Expand Down Expand Up @@ -55,7 +59,50 @@ 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))
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{}
Expand All @@ -66,14 +113,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)
Expand Down Expand Up @@ -128,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),
Namespace: r.Header.Get(Namespace),
IngressName: r.Header.Get(IngressName),
ServiceName: r.Header.Get(ServiceName),
OriginalURI: originalURI,
Namespace: ns,
IngressName: ingress.Name,
ServiceName: serviceName,
ServicePort: r.Header.Get(ServicePort),
RequestID: r.Header.Get(RequestID),
RefreshInterval: h.RefreshInterval,
Expand Down Expand Up @@ -210,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)
}
2 changes: 2 additions & 0 deletions internal/handlers/unidler/unidler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion resources/html/unidle.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Preparing your environment | Lagoon</title>
<meta http-equiv="refresh" content="{{ .RefreshInterval }}">
<meta http-equiv="refresh" content="{{ .RefreshInterval }} url={{ .OriginalURI }}">
<meta name="robots" content="noindex, nofollow">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<style>
Expand Down
Loading