From 09a03cf4a077cc58570ceb1cbd81fdc171ba4195 Mon Sep 17 00:00:00 2001 From: shreddedbacon Date: Thu, 14 Dec 2023 11:18:49 +1100 Subject: [PATCH] refactor: allow overrides for allow/blocks on namespace annotations --- README.md | 4 +- handlers/idler/service-kubernetes.go | 6 +- handlers/unidler/handler.go | 11 ++- handlers/unidler/restrictions.go | 102 ++++++++++++-------------- handlers/unidler/restrictions_test.go | 22 +++++- handlers/unidler/unidler.go | 3 +- main.go | 4 + 7 files changed, 86 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 4568cf9..3cfee99 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ It is possible to add global IP allow and block lists, the helm chart will have * allowing IP addresses via `/lists/allowedips` file which is a single line per entry of ip address to allow * blocking IP addresses via `/lists/blockedips` file which is a single line per entry of ip address to block -There are also annotations that can be added to specific `Kind: Ingress` objects that allow for ip allow or blocking. +There are also annotations that can be added to the namespace, or individual `Kind: Ingress` objects that allow for ip allow or blocking. * `idling.amazee.io/ip-allow-list` - a comma separated list of ip addresses to allow, will be checked against x-forward-for, but if true-client-ip is provided it will prefer this. * `idling.amazee.io/ip-block-list` - a comma separated list of ip addresses to allow, will be checked against x-forward-for, but if true-client-ip is provided it will prefer this. @@ -40,7 +40,7 @@ It is possible to add global UserAgent allow and block lists, the helm chart wil * allowing user agents via a `/lists/allowedagents` file which is a single line per entry of useragents or regex patterns to match against. These must be `go` based regular expressions. * blocking user agents via a `/lists/blockedagents` file which is a single line per entry of useragents or regex patterns to match against. These must be `go` based regular expressions. -There are also annotations that can be added to specific `Kind: Ingress` objects that allow for user agent allow or blocking. +There are also annotations that can be added to the namespace, or individual `Kind: Ingress` objects that allow for user agent allow or blocking. * `idling.amazee.io/allowed-agents` - a comma separated list of user agents or regex patterns to allow. * `idling.amazee.io/blocked-agents` - a comma separated list of user agents or regex patterns to block. diff --git a/handlers/idler/service-kubernetes.go b/handlers/idler/service-kubernetes.go index 9195d97..dc9c933 100644 --- a/handlers/idler/service-kubernetes.go +++ b/handlers/idler/service-kubernetes.go @@ -76,8 +76,6 @@ func (h *Idler) KubernetesServiceIdler(ctx context.Context, opLog logr.Logger, n opLog.Error(err, fmt.Sprintf("Error getting deployments")) return } - // fmt.Println(labelRequirements) - // fmt.Println("deploys", len(deployments.Items)) for _, deployment := range deployments.Items { checkPods := false zeroReps := new(int32) @@ -134,11 +132,11 @@ func (h *Idler) KubernetesServiceIdler(ctx context.Context, opLog logr.Logger, n ) result, warnings, err := v1api.Query(ctx, promQuery, time.Now()) if err != nil { - fmt.Printf("Error querying Prometheus: %v\n", err) + opLog.Error(err, "Error querying Prometheus") return } if len(warnings) > 0 { - fmt.Printf("Warnings: %v\n", warnings) + opLog.Info(fmt.Sprintf("Warnings: %v", warnings)) } // and then add up the results of all the status requests to determine hit count if result.Type() == prometheusmodel.ValVector { diff --git a/handlers/unidler/handler.go b/handlers/unidler/handler.go index e37d7aa..841b9dd 100644 --- a/handlers/unidler/handler.go +++ b/handlers/unidler/handler.go @@ -12,6 +12,7 @@ import ( "time" "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" networkv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/types" ) @@ -66,6 +67,13 @@ func (h *Unidler) ingressHandler(path string) func(http.ResponseWriter, *http.Re // @TODO: check for code 503 specifically, or just any request that has the namespace in it will be "unidled" if a request comes in for // that ingress and the if ns != "" { + namespace := &corev1.Namespace{} + if err := h.Client.Get(ctx, types.NamespacedName{ + Name: ns, + }, namespace); err != nil { + opLog.Info(fmt.Sprintf("unable to get any namespaces: %v", err)) + return + } ingress := &networkv1.Ingress{} if err := h.Client.Get(ctx, types.NamespacedName{ Namespace: ns, @@ -81,7 +89,7 @@ func (h *Unidler) ingressHandler(path string) func(http.ResponseWriter, *http.Re trueClientIP := r.Header.Get("True-Client-IP") requestUserAgent := r.Header.Get("User-Agent") - allowUnidle := h.checkAccess(ingress.ObjectMeta.Annotations, requestUserAgent, trueClientIP, xForwardedFor) + allowUnidle := h.checkAccess(namespace.ObjectMeta.Annotations, ingress.ObjectMeta.Annotations, requestUserAgent, trueClientIP, xForwardedFor) // then run checks to start to unidle the environment if allowUnidle { // if a namespace exists, it means that the custom-http-errors code is defined in the ingress object @@ -105,6 +113,7 @@ func (h *Unidler) ingressHandler(path string) func(http.ResponseWriter, *http.Re if h.Debug == true { opLog.Info(fmt.Sprintf("Serving custom error response for code %v and format %v from file %v", code, format, file)) } + w.Header().Set("X-Aergia-Allowed", "true") // then return the unidle template to the user tmpl := template.Must(template.ParseFiles(file)) tmpl.ExecuteTemplate(w, "base", pageData{ diff --git a/handlers/unidler/restrictions.go b/handlers/unidler/restrictions.go index 45e7977..9adde9a 100644 --- a/handlers/unidler/restrictions.go +++ b/handlers/unidler/restrictions.go @@ -34,75 +34,63 @@ func checkIPList(allowList []string, xForwardedFor []string, trueClientIP string return false } -func (h *Unidler) checkAccess(annotations map[string]string, userAgent, trueClientIP string, xForwardedFor []string) bool { - allowedIP := false - allowedAgent := false - blockedIP := false - blockedAgent := false - - hasIPAllowList := false - hasAllowedAgentList := false - hasIPBlockList := false - hasBlockedAgentList := false - - if alist, ok := annotations["idling.amazee.io/ip-allow-list"]; ok { - // there is an allow list, we want to deny any requests now unless they are the trueclientip - // or xforwardedfor if trueclientip is not defined - hasIPAllowList = true - allowedIP = checkIPList(strings.Split(alist, ","), xForwardedFor, trueClientIP) - } else { - if h.AllowedIPs != nil { - hasIPAllowList = true - allowedIP = checkIPList(h.AllowedIPs, xForwardedFor, trueClientIP) - } - } - - if blist, ok := annotations["idling.amazee.io/ip-block-list"]; ok { - // there is a block list, we want to allow any requests now unless they are the trueclientip - // or xforwardedfor if trueclientip is not defined - hasIPBlockList = true - blockedIP = checkIPList(strings.Split(blist, ","), xForwardedFor, trueClientIP) - } else { - if h.BlockedIPs != nil { - hasIPBlockList = true - blockedIP = checkIPList(h.BlockedIPs, xForwardedFor, trueClientIP) - } - } +func (h *Unidler) checkAccess(nsannotations map[string]string, annotations map[string]string, userAgent, trueClientIP string, xForwardedFor []string) bool { // deal with ip allow/blocks first - if allowedIP && hasIPAllowList { + blockedIP := checkIPAnnotations("idling.amazee.io/ip-block-list", trueClientIP, xForwardedFor, h.BlockedIPs, nsannotations, annotations) + if blockedIP { + return false + } + allowedIP := checkIPAnnotations("idling.amazee.io/ip-allow-list", trueClientIP, xForwardedFor, h.AllowedIPs, nsannotations, annotations) + if allowedIP { return true } - if blockedIP && hasIPBlockList { + blockedAgent := checkAgentAnnotations("idling.amazee.io/blocked-agents", userAgent, h.BlockedUserAgents, nsannotations, annotations) + if blockedAgent { return false } + allowedAgent := checkAgentAnnotations("idling.amazee.io/allowed-agents", userAgent, h.AllowedUserAgents, nsannotations, annotations) + if allowedAgent { + return true + } + // else fallthrough + return true +} - if agents, ok := annotations["idling.amazee.io/allowed-agents"]; ok { - hasAllowedAgentList = true - allowedAgent = checkAgents(strings.Split(agents, ","), userAgent) +func checkAgentAnnotations(annotation, ua string, g []string, ns, i map[string]string) bool { + allow := false + if agents, ok := i[annotation]; ok { + allow = checkAgents(strings.Split(agents, ","), ua) } else { - if h.AllowedUserAgents != nil { - hasAllowedAgentList = true - allowedAgent = checkAgents(h.AllowedUserAgents, userAgent) + // check for namespace annoation + if agents, ok := ns[annotation]; ok { + allow = checkAgents(strings.Split(agents, ","), ua) + } else { + // check for globals + if g != nil { + allow = checkAgents(g, ua) + } } } + return allow +} - if agents, ok := annotations["idling.amazee.io/blocked-agents"]; ok { - hasBlockedAgentList = true - blockedAgent = checkAgents(strings.Split(agents, ","), userAgent) +func checkIPAnnotations(annotation, tcip string, xff, g []string, ns, i map[string]string) bool { + allow := false + if alist, ok := i[annotation]; ok { + // there is an allow list, we want to deny any requests now unless they are the trueclientip + // or xforwardedfor if trueclientip is not defined + allow = checkIPList(strings.Split(alist, ","), xff, tcip) } else { - if h.BlockedUserAgents != nil { - hasBlockedAgentList = true - blockedAgent = checkAgents(h.BlockedUserAgents, userAgent) + // check for namespace annoation + if alist, ok := ns[annotation]; ok { + allow = checkIPList(strings.Split(alist, ","), xff, tcip) + } else { + // check for globals + if g != nil { + allow = checkIPList(g, xff, tcip) + } } } - - if allowedAgent && hasAllowedAgentList { - return true - } - if blockedAgent && hasBlockedAgentList { - return false - } - // else fallthrough - return true + return allow } diff --git a/handlers/unidler/restrictions_test.go b/handlers/unidler/restrictions_test.go index 44916a0..a8cc841 100644 --- a/handlers/unidler/restrictions_test.go +++ b/handlers/unidler/restrictions_test.go @@ -116,6 +116,7 @@ func TestUnidler_checkAccess(t *testing.T) { BlockedIPs []string } type args struct { + nsannotations map[string]string annotations map[string]string userAgent string trueClientIP string @@ -362,6 +363,25 @@ func TestUnidler_checkAccess(t *testing.T) { }, want: true, }, + { + name: "test15 - allowed ip blocked agent namespace annotation", + args: args{ + nsannotations: map[string]string{ + "idling.amazee.io/blocked-agents": "@(example).test.?$,@(internal).test.?$", + "idling.amazee.io/ip-allow-list": "1.2.3.4", + }, + userAgent: "This is not a bot, don't complaint to: complain@example.test.", + trueClientIP: "1.2.3.4", + xForwardedFor: nil, + }, + fields: fields{ + AllowedUserAgents: nil, + BlockedUserAgents: nil, + BlockedIPs: nil, + AllowedIPs: nil, + }, + want: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -371,7 +391,7 @@ func TestUnidler_checkAccess(t *testing.T) { AllowedIPs: tt.fields.AllowedIPs, BlockedIPs: tt.fields.BlockedIPs, } - if got := h.checkAccess(tt.args.annotations, tt.args.userAgent, tt.args.trueClientIP, tt.args.xForwardedFor); got != tt.want { + if got := h.checkAccess(tt.args.nsannotations, tt.args.annotations, tt.args.userAgent, tt.args.trueClientIP, tt.args.xForwardedFor); got != tt.want { t.Errorf("Unidler.checkAccess() = %v, want %v", got, tt.want) } }) diff --git a/handlers/unidler/unidler.go b/handlers/unidler/unidler.go index 1ec9589..76d95df 100644 --- a/handlers/unidler/unidler.go +++ b/handlers/unidler/unidler.go @@ -36,6 +36,7 @@ type Unidler struct { Client ctrlClient.Client Log logr.Logger RefreshInterval int + UnidlerHTTPPort int Debug bool RequestCount *prometheus.CounterVec RequestDuration *prometheus.HistogramVec @@ -106,7 +107,7 @@ func Run(h *Unidler, setupLog logr.Logger) { http.Handle("/", r) httpServer := &http.Server{ - Addr: ":5000", + Addr: fmt.Sprintf(":%d", h.UnidlerHTTPPort), Handler: r, } err := httpServer.ListenAndServe() diff --git a/main.go b/main.go index ce45513..b5c60a2 100644 --- a/main.go +++ b/main.go @@ -56,6 +56,7 @@ func main() { var enableLeaderElection bool var debug bool var refreshInterval int + var unidlerHTTPPort int var dryRun bool var selectorsFile string @@ -94,12 +95,14 @@ func main() { "Flag to determine if the idler should check the hit backend or not. If true, this overrides what is in the selectors file.") flag.BoolVar(&enableCLIIdler, "enable-cli-idler", true, "Flag to enable cli idler.") flag.BoolVar(&enableServiceIdler, "enable-service-idler", true, "Flag to enable service idler.") + flag.IntVar(&unidlerHTTPPort, "unidler-port", 5000, "Port for the unidler service to listen on.") flag.Parse() selectorsFile = variables.GetEnv("SELECTORS_YAML_FILE", selectorsFile) dryRun = variables.GetEnvBool("DRY_RUN", dryRun) + unidlerHTTPPort = variables.GetEnvInt("UNIDLER_PORT", unidlerHTTPPort) cliCron = variables.GetEnv("CLI_CRON", cliCron) serviceCron = variables.GetEnv("SERVICE_CRON", serviceCron) enableServiceIdler = variables.GetEnvBool("ENABLE_SERVICE_IDLER", enableServiceIdler) @@ -178,6 +181,7 @@ func main() { BlockedUserAgents: blockedAgents, AllowedIPs: allowedIPs, BlockedIPs: blockedIPs, + UnidlerHTTPPort: unidlerHTTPPort, } prometheusClient, err := prometheusapi.NewClient(prometheusapi.Config{