From f728fc8cd7cdfae40c2b6c2e338a5b6da64dc4ad Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Wed, 29 Apr 2026 01:10:34 -0400 Subject: [PATCH] fix: only emit keda fallback when a non-cpu/memory trigger exists Keda 2.17+ rejects ScaledObjects whose spec.fallback is set unless at least one trigger is not a cpu or memory scaler, so unconditionally emitting fallback broke deploys for apps autoscaled on cpu or memory alone. The chart now skips the fallback block when every configured trigger is cpu or memory and keeps the existing behavior otherwise. --- docs/deployment/schedulers/k3s.md | 2 + .../templates/chart/keda-scaled-object.yaml | 8 ++ tests/unit/scheduler-k3s-4.bats | 117 ++++++++++++++++++ 3 files changed, 127 insertions(+) diff --git a/docs/deployment/schedulers/k3s.md b/docs/deployment/schedulers/k3s.md index c28ac80df..33e55126b 100644 --- a/docs/deployment/schedulers/k3s.md +++ b/docs/deployment/schedulers/k3s.md @@ -476,6 +476,8 @@ Each value in the `metadata` stanza can use the following interpolated strings: - `DOKKU_PROCESS_TYPE`: The name of the process being scaled - `DOKKU_APP_NAME`: The name of the app being scaled +Dokku configures a Keda `fallback` (with a failure threshold of `3` and the configured replica count) on the generated `ScaledObject` so that scaling falls back to the configured replica count when a scaler's metric source becomes unavailable. Keda only allows `fallback` when at least one trigger is something other than `cpu` or `memory`, so Dokku omits the `fallback` block when every configured trigger is a `cpu` or `memory` scaler. + ##### HTTP Autoscaling In addition to the built-in scalers that Keda provides, Dokku also supports Keda's HTTP Add On. This requires that the addon be properly installed and configured. For existing k3s clusters, this can be performed by the `scheduler-k3s:ensure-charts` command: diff --git a/plugins/scheduler-k3s/templates/chart/keda-scaled-object.yaml b/plugins/scheduler-k3s/templates/chart/keda-scaled-object.yaml index ac0f61f23..221f054e1 100644 --- a/plugins/scheduler-k3s/templates/chart/keda-scaled-object.yaml +++ b/plugins/scheduler-k3s/templates/chart/keda-scaled-object.yaml @@ -33,9 +33,17 @@ spec: pollingInterval: {{ $config.autoscaling.polling_interval_seconds }} minReplicaCount: {{ $config.autoscaling.min_replicas }} maxReplicaCount: {{ $config.autoscaling.max_replicas }} + {{- $hasFallbackTrigger := false }} + {{- range $config.autoscaling.triggers }} + {{- if and (ne .type "cpu") (ne .type "memory") }} + {{- $hasFallbackTrigger = true }} + {{- end }} + {{- end }} + {{- if $hasFallbackTrigger }} fallback: failureThreshold: 3 replicas: {{ $config.replicas }} + {{- end }} {{- if $config.autoscaling.triggers }} triggers: {{- with $config.autoscaling.triggers }} diff --git a/tests/unit/scheduler-k3s-4.bats b/tests/unit/scheduler-k3s-4.bats index 193ec1736..740f56901 100644 --- a/tests/unit/scheduler-k3s-4.bats +++ b/tests/unit/scheduler-k3s-4.bats @@ -52,6 +52,66 @@ teardown() { assert_output "5000" } +@test "(scheduler-k3s) deploy autoscaling cpu-only trigger [keda-fallback]" { + if [[ -z "$DOCKERHUB_USERNAME" ]] || [[ -z "$DOCKERHUB_TOKEN" ]]; then + skip "skipping due to missing docker.io credentials DOCKERHUB_USERNAME:DOCKERHUB_TOKEN" + fi + + INGRESS_CLASS=nginx install_k3s + + run /bin/bash -c "dokku apps:create $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + + run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP inject_cpu_autoscaling_app_json + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "kubectl get scaledobjects.keda.sh $TEST_APP-web -o=jsonpath='{.spec.fallback}'" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "" + + run /bin/bash -c "kubectl get scaledobjects.keda.sh $TEST_APP-web -o=jsonpath='{.spec.triggers[0].type}'" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "cpu" +} + +@test "(scheduler-k3s) deploy autoscaling external trigger [keda-fallback]" { + if [[ -z "$DOCKERHUB_USERNAME" ]] || [[ -z "$DOCKERHUB_TOKEN" ]]; then + skip "skipping due to missing docker.io credentials DOCKERHUB_USERNAME:DOCKERHUB_TOKEN" + fi + + INGRESS_CLASS=nginx install_k3s + + run /bin/bash -c "dokku apps:create $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + + run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP inject_prometheus_autoscaling_app_json + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "kubectl get scaledobjects.keda.sh $TEST_APP-web -o=jsonpath='{.spec.fallback.failureThreshold}'" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "3" + + run /bin/bash -c "kubectl get scaledobjects.keda.sh $TEST_APP-web -o=jsonpath='{.spec.fallback.replicas}'" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "1" +} + inject_app_json() { local APP="$1" local APP_REPO_DIR="$2" @@ -68,3 +128,60 @@ inject_app_json() { } EOF } + +inject_cpu_autoscaling_app_json() { + local APP="$1" + local APP_REPO_DIR="$2" + [[ -z "$APP" ]] && local APP="$TEST_APP" + cat <"$APP_REPO_DIR/app.json" +{ + "formation": { + "web": { + "autoscaling": { + "min_quantity": 1, + "max_quantity": 3, + "triggers": [ + { + "name": "cpu-trigger", + "type": "cpu", + "metadata": { + "value": "80" + } + } + ] + } + } + } +} +EOF +} + +inject_prometheus_autoscaling_app_json() { + local APP="$1" + local APP_REPO_DIR="$2" + [[ -z "$APP" ]] && local APP="$TEST_APP" + cat <"$APP_REPO_DIR/app.json" +{ + "formation": { + "web": { + "autoscaling": { + "min_quantity": 1, + "max_quantity": 3, + "triggers": [ + { + "name": "prom-trigger", + "type": "prometheus", + "metadata": { + "serverAddress": "http://prometheus.example.com:9090", + "metricName": "http_requests_total", + "threshold": "100", + "query": "sum(rate(http_requests_total{deployment=\"[[ .DEPLOYMENT_NAME ]]\"}[2m]))" + } + } + ] + } + } + } +} +EOF +}