Skip to content

K8s: Implement KEDA external scaler and use it as default#3169

Merged
VietND96 merged 5 commits into
trunkfrom
keda-external-scaler
Jul 13, 2026
Merged

K8s: Implement KEDA external scaler and use it as default#3169
VietND96 merged 5 commits into
trunkfrom
keda-external-scaler

Conversation

@VietND96

@VietND96 VietND96 commented Jul 11, 2026

Copy link
Copy Markdown
Member

Thanks for contributing to the Docker-Selenium project!
A PR well described will help maintainers to quickly review and merge it

Before submitting your PR, please check our contributing guidelines, applied for this repository.
Avoid large PRs, help reviewers by making them as simple and short as possible.

Description

Instead of relying on the built-in Selenium Grid scaler in the KEDA core package.
Use the concept of External Scaler (https://keda.sh/docs/latest/concepts/external-scalers/), where implementing the logic on top of KEDA, any logic change can be patched independently in the external scaler managed

Motivation and Context

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • I have read the contributing document.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

@VietND96
VietND96 force-pushed the keda-external-scaler branch from be58cc2 to be29ccf Compare July 11, 2026 17:52
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

K8s: Add standalone KEDA external scaler for Selenium Grid autoscaling

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Adds a standalone Selenium Grid KEDA external scaler gRPC server with parity tests.
• Updates Helm chart to deploy the scaler and switch triggers to type: external.
• Extends CI and release Makefile to build, test, and publish the scaler image.
Diagram

graph TD
  keda{{"KEDA"}} -->|"gRPC"| svc["Scaler Service"] --> pod["External scaler"] -->|"GraphQL HTTP"| grid["Selenium Grid"]
  helm["Helm chart"] --> deploy["Scaler Deployment"] --> pod
  helm --> svc --> pod
  secrets[("K8s Secrets")] --> pod

  subgraph Legend
    direction LR
    _ext{{"External"}} ~~~ _comp["Component"] ~~~ _sec[("Secret")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep using KEDA built-in `selenium-grid` scaler only
  • ➕ No additional Deployment/Service to operate
  • ➕ No new image to build, scan, and publish
  • ➖ Scaling fixes ship on KEDA cadence rather than docker-selenium cadence
  • ➖ Harder to keep scaler logic evolving in lockstep with Grid behavior for this project’s users
2. Upstream all fixes to KEDA and require KEDA upgrades
  • ➕ Single canonical implementation remains in KEDA
  • ➕ Avoids owning a separate runtime component
  • ➖ Still blocked by KEDA release timelines and operator upgrade cycles
  • ➖ Limits ability to ship urgent autoscaling fixes with docker-selenium releases
3. Fork KEDA controller/scalers instead of external scaler
  • ➕ No gRPC/service indirection; keeps internal scaler model
  • ➖ High operational and maintenance burden (own a KEDA fork)
  • ➖ More compatibility risk across KEDA versions than the stable external-scaler proto contract

Recommendation: The PR’s approach (standalone external scaler + Helm opt-in wiring) is the best fit for docker-selenium: it decouples scaling-logic releases from KEDA while preserving behavior via upstream-parity test gates and preserving metric naming for smooth migration. The added operational surface area (one Deployment/Service) is contained and configurable through the chart.

Files changed (37) +6765 / -9

Enhancement (7) +979 / -0
main.goImplement scaler entrypoint with TLS, health, and graceful shutdown +157/-0

Implement scaler entrypoint with TLS, health, and graceful shutdown

• Starts the gRPC server, registers grpc_health_v1, optionally enables server-side TLS, and shuts down gracefully on SIGTERM/SIGINT. Collects Grid URL/credentials from environment as server-side fallbacks.

.keda-external-scaler/cmd/scaler/main.go

grid_client.goAdd Selenium Grid GraphQL client with auth and unsafeSsl support +101/-0

Add Selenium Grid GraphQL client with auth and unsafeSsl support

• Implements the exact GraphQL query used by the built-in KEDA scaler, supports Basic or bearer-style auth, and reuses http.Client instances per unsafeSsl setting to avoid repeated TLS handshakes.

.keda-external-scaler/internal/gridscaler/grid_client.go

logic.goPort scaler core counting and capability-matching logic +270/-0

Port scaler core counting and capability-matching logic

• Ports capability filtering/matching, slot reservation, platform-family logic, and node count computation from Grid GraphQL responses to match KEDA’s built-in behavior.

.keda-external-scaler/internal/gridscaler/logic.go

metadata.goAdd trigger metadata parsing with env fallback precedence +148/-0

Add trigger metadata parsing with env fallback precedence

• Parses KEDA scalerMetadata into a typed Metadata struct with defaults, required URL validation, and jobScalingStrategy validation. Adds scaler-side env fallback (SE_GRID_URL, SE_USERNAME, etc.) because TriggerAuthentication authParams are not forwarded to external scalers.

.keda-external-scaler/internal/gridscaler/metadata.go

platform.goPort Selenium Platform mapping and family checks +112/-0

Port Selenium Platform mapping and family checks

• Implements the platform mapping table and family comparison used during capability matching to keep semantics aligned with Selenium Grid.

.keda-external-scaler/internal/gridscaler/platform.go

server.goImplement KEDA ExternalScaler gRPC service for Selenium Grid +129/-0

Implement KEDA ExternalScaler gRPC service for Selenium Grid

• Implements GetMetricSpec, GetMetrics, and IsActive with KEDA-compatible metric naming and jobScalingStrategy conventions. Returns Unimplemented for StreamIsActive (external-push unsupported for Grid).

.keda-external-scaler/internal/gridscaler/server.go

types.goAdd typed GraphQL response models and capability constants +62/-0

Add typed GraphQL response models and capability constants

• Defines Go structs mirroring the Grid GraphQL response shape and capability-prefix constants used by matching logic.

.keda-external-scaler/internal/gridscaler/types.go

Tests (4) +3831 / -0
grid_client_test.goTest GraphQL query, auth behavior, and client caching +101/-0

Test GraphQL query, auth behavior, and client caching

• Validates POST body/query string, Basic and bearer auth, non-200 error handling, and caching behavior via httptest servers.

.keda-external-scaler/internal/gridscaler/grid_client_test.go

logic_test.goAdd upstream KEDA parity test table for counting logic +3168/-0

Add upstream KEDA parity test table for counting logic

• Ports KEDA’s selenium-grid scaler test table verbatim to lock behavior and expected values, acting as the primary parity gate against upstream.

.keda-external-scaler/internal/gridscaler/logic_test.go

metadata_test.goTest metadata defaults, validation, and env resolution precedence +342/-0

Test metadata defaults, validation, and env resolution precedence

• Covers missing/invalid fields, default behavior, jobScalingStrategy validation, and precedence across direct keys, *FromEnv keys, and scaler environment fallbacks.

.keda-external-scaler/internal/gridscaler/metadata_test.go

server_test.goAdd in-process gRPC server tests with fake Grid fixtures +220/-0

Add in-process gRPC server tests with fake Grid fixtures

• Verifies metric naming normalization, count semantics for all jobScalingStrategy modes, error code mapping, StreamIsActive unimplemented behavior, and scaler-side env fallback usage.

.keda-external-scaler/internal/gridscaler/server_test.go

Documentation (10) +765 / -0
PLAN.mdDocument implementation plan and parity gates +79/-0

Document implementation plan and parity gates

• Adds a step-by-step plan with dependency ordering, design decisions (metric naming, env fallback), and verification checkpoints to prevent drift from KEDA behavior.

.keda-external-scaler/PLAN.md

README.mdDocument external scaler usage, metadata, and auth model +114/-0

Document external scaler usage, metadata, and auth model

• Explains how the external scaler maps Grid GraphQL state to scaling decisions and how it differs from KEDA’s built-in scaler. Documents metadata keys, auth precedence (metadata/*FromEnv/scaler env), deployment examples, and runtime flags.

.keda-external-scaler/README.md

SPEC.mdAdd spec for extracting KEDA selenium-grid scaler into external scaler +262/-0

Add spec for extracting KEDA selenium-grid scaler into external scaler

• Defines objectives, the external scaler gRPC contract constraints, and the exact built-in logic that must be ported for parity. Covers TLS expectations and metric naming continuity.

.keda-external-scaler/SPEC.md

TASKS.mdAdd task breakdown for spec-driven workflow +81/-0

Add task breakdown for spec-driven workflow

• Introduces an ordered task list (T1–T9) with acceptance criteria and verification commands for scaffolding, parity tests, server, packaging, and docs.

.keda-external-scaler/TASKS.md

deployment.yamlAdd reference Deployment + Service manifests for the scaler +82/-0

Add reference Deployment + Service manifests for the scaler

• Provides Kubernetes manifests to deploy the scaler and expose it over a Service. Demonstrates providing Grid URL and optional credentials via environment variables/secrets.

.keda-external-scaler/deploy/deployment.yaml

scaledjob-example.yamlAdd ScaledJob example wired to external scaler +49/-0

Add ScaledJob example wired to external scaler

• Shows a ScaledJob using trigger 'type: external' with 'scalerAddress', plus selenium-grid metadata. Notes that jobScalingStrategy must match the ScaledJob scalingStrategy.

.keda-external-scaler/deploy/scaledjob-example.yaml

scaledobject-example.yamlAdd ScaledObject example wired to external scaler +24/-0

Add ScaledObject example wired to external scaler

• Shows a ScaledObject trigger using 'type: external' and 'scalerAddress', reusing the familiar selenium-grid metadata keys.

.keda-external-scaler/deploy/scaledobject-example.yaml

plan.mdAdd internal plan note for scaler tasks +10/-0

Add internal plan note for scaler tasks

• Adds a short planning note under the scaler’s tasks directory.

.keda-external-scaler/tasks/plan.md

todo.mdAdd internal TODO list for scaler follow-ups +18/-0

Add internal TODO list for scaler follow-ups

• Introduces a TODO checklist for future work related to the external scaler.

.keda-external-scaler/tasks/todo.md

README.mdDocument Helm option to use the standalone external scaler +46/-0

Document Helm option to use the standalone external scaler

• Documents enabling autoscaling.externalScaler, what resources it deploys, and how triggers are rewired to 'type: external'. Explains the auth difference (secrets mounted into scaler env) and migration/rollback expectations.

charts/selenium-grid/README.md

Other (16) +1190 / -9
helm-chart-test.ymlRun Helm chart CI with external scaler enabled +4/-2

Run Helm chart CI with external scaler enabled

• Updates the workflow matrix to exercise autoscaling with the external scaler enabled. Propagates TEST_EXTERNAL_SCALER and adjusts artifact naming to distinguish runs.

.github/workflows/helm-chart-test.yml

.dockerignoreAdd dockerignore for scaler build context +7/-0

Add dockerignore for scaler build context

• Excludes build outputs, docs, deploy examples, and git metadata from the Docker build context to reduce image build noise and size.

.keda-external-scaler/.dockerignore

DockerfileAdd multi-stage distroless build for external scaler +30/-0

Add multi-stage distroless build for external scaler

• Builds a static Go binary via buildx multi-arch args and packages it into a distroless nonroot image. Exposes the gRPC port and sets the scaler binary as entrypoint.

.keda-external-scaler/Dockerfile

MakefileAdd build/test/proto/image targets for scaler module +61/-0

Add build/test/proto/image targets for scaler module

• Provides local developer targets for build, test, vet/lint, protobuf codegen, and multi-arch image builds. Pins protoc-gen-go and protoc-gen-go-grpc versions for deterministic generation.

.keda-external-scaler/Makefile

externalscaler.pb.goCommit generated protobuf message types for ExternalScaler API +539/-0

Commit generated protobuf message types for ExternalScaler API

• Adds protoc-generated Go types matching KEDA’s external scaler proto so consumers can build without protoc installed.

.keda-external-scaler/externalscaler/externalscaler.pb.go

externalscaler_grpc.pb.goCommit generated gRPC stubs for ExternalScaler service +239/-0

Commit generated gRPC stubs for ExternalScaler service

• Adds protoc-gen-go-grpc output defining the ExternalScaler client/server interfaces and handlers used by the scaler implementation.

.keda-external-scaler/externalscaler/externalscaler_grpc.pb.go

go.modIntroduce standalone Go module for the external scaler +16/-0

Introduce standalone Go module for the external scaler

• Creates a dedicated Go module targeting Go 1.26 and pins grpc/protobuf/logr dependencies required to implement the service.

.keda-external-scaler/go.mod

go.sumAdd checksums for external scaler module dependencies +34/-0

Add checksums for external scaler module dependencies

• Adds go.sum entries for direct and indirect dependencies needed for building and testing the scaler.

.keda-external-scaler/go.sum

externalscaler.protoVendor KEDA externalscaler.proto contract +51/-0

Vendor KEDA externalscaler.proto contract

• Adds the ExternalScaler proto definition (byte-identical to KEDA) as the source for generated stubs committed in this repo.

.keda-external-scaler/proto/externalscaler.proto

MakefileAdd build/tag/push support for keda-external-scaler image +25/-5

Add build/tag/push support for keda-external-scaler image

• Adds a root Makefile target to build the scaler image with the project’s standard tagging scheme. Extends latest/nightly/major-minor tagging and GHCR publishing loops to include keda-external-scaler.

Makefile

_helpers.tplSwitch KEDA triggers to external scaler when enabled +7/-1

Switch KEDA triggers to external scaler when enabled

• Changes the default trigger type to 'external' when autoscaling.externalScaler.enabled is true, injects scalerAddress metadata, and omits authenticationRef in external-scaler mode.

charts/selenium-grid/templates/_helpers.tpl

_nameHelpers.tplAdd Helm helpers for external scaler naming and address +14/-0

Add Helm helpers for external scaler naming and address

• Adds templates to compute the scaler resource full name and its Service address (host:port) for use in trigger metadata.

charts/selenium-grid/templates/_nameHelpers.tpl

external-scaler.yamlAdd Helm template for external scaler Deployment + Service +116/-0

Add Helm template for external scaler Deployment + Service

• Introduces a Deployment/Service for the scaler, with configurable image/replicas/resources and gRPC probes. Mounts Grid URL and optional basic-auth credentials from existing chart Secrets into scaler env.

charts/selenium-grid/templates/external-scaler.yaml

trigger-auth.yamlDisable TriggerAuthentication rendering in external-scaler mode +1/-1

Disable TriggerAuthentication rendering in external-scaler mode

• Updates template condition so TriggerAuthentication is not created when autoscaling.externalScaler.enabled is true (it is unused because authParams are not forwarded).

charts/selenium-grid/templates/trigger-auth.yaml

values.yamlAdd autoscaling.externalScaler configuration block +39/-0

Add autoscaling.externalScaler configuration block

• Adds values to enable and configure the external scaler (image settings, port, replicas, timeouts, env, scheduling, resources) and documents secret-mount behavior for Grid URL/credentials.

charts/selenium-grid/values.yaml

chart_test.shAdd chart test toggle to enable external scaler +7/-0

Add chart test toggle to enable external scaler

• Adds TEST_EXTERNAL_SCALER and, when enabled with autoscaling, injects '--set autoscaling.externalScaler.enabled=true' into the Helm test command.

tests/charts/make/chart_test.sh

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Panic on bad capabilities 🐞 Bug ☼ Reliability
Description
In getCountFromSeleniumResponse, a JSON unmarshal error for trigger metadata "capabilities" is
logged but execution continues and writes into the returned map; when parseCapabilitiesToMap returns
nil, this assignment panics and crashes the scaler pod, breaking KEDA scaling.
Code

.keda-external-scaler/internal/gridscaler/logic.go[R192-198]

+	capabilities, err := parseCapabilitiesToMap(_capabilities)
+	if err != nil {
+		logger.Error(err, fmt.Sprintf("Error when unmarshaling trigger metadata 'capabilities': %s", err))
+	}
+	if enableManagedDownloads {
+		capabilities[EnableManagedDownloadsCapability] = true
+	}
Evidence
The parsing helper can return a nil map on JSON errors, and the counting function writes into that
map without guarding; because EnableManagedDownloads defaults to true, the write path is commonly
executed. The spec explicitly requires not panicking on malformed metadata.

.keda-external-scaler/internal/gridscaler/logic.go[37-45]
.keda-external-scaler/internal/gridscaler/logic.go[192-198]
.keda-external-scaler/internal/gridscaler/metadata.go[82-87]
.keda-external-scaler/SPEC.md[219-223]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`parseCapabilitiesToMap` returns `nil, err` on invalid JSON, but `getCountFromSeleniumResponse` logs the error and continues, then writes into the (possibly nil) map when `enableManagedDownloads` is true (default). This can panic and crash the scaler, violating the module's own "never panic on malformed metadata" boundary.

## Issue Context
The `capabilities` string comes from scaler metadata, so malformed input should produce a gRPC `InvalidArgument` (via `parseMetadata`) rather than a process crash.

## Fix Focus Areas
- .keda-external-scaler/internal/gridscaler/logic.go[37-45]
- .keda-external-scaler/internal/gridscaler/logic.go[192-198]
- .keda-external-scaler/internal/gridscaler/metadata.go[98-103]
- .keda-external-scaler/internal/gridscaler/metadata.go[82-87]

### Suggested fix approach
1. Validate `capabilities` JSON in `parseMetadata` when the string is non-empty (attempt `json.Unmarshal` into a `map[string]any]`); on failure return an error so RPCs map to `codes.InvalidArgument`.
2. In `getCountFromSeleniumResponse`, treat `parseCapabilitiesToMap` errors as fatal (return the error) and/or ensure the returned map is never nil before assignment.
3. Add/extend tests covering invalid `capabilities` (expect `InvalidArgument`, and no panic).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Metric name not normalized 🐞 Bug ≡ Correctness
Description
buildMetricName claims to produce an HPA-safe metric name, but normalizeString does not handle
spaces/uppercase; platformName values like "Windows 11" will yield metric names containing spaces.
Code

.keda-external-scaler/internal/gridscaler/server.go[R107-129]

+// buildMetricName mirrors the built-in scaler's naming so HPA metric identity is
+// preserved across migration: selenium-grid[-browser][-version][-platform],
+// normalized to an HPA-safe string.
+func buildMetricName(meta *Metadata) string {
+	nameParts := []string{"selenium-grid"}
+	if meta.BrowserName != "" {
+		nameParts = append(nameParts, meta.BrowserName)
+	}
+	if meta.BrowserVersion != "" {
+		nameParts = append(nameParts, meta.BrowserVersion)
+	}
+	if meta.PlatformName != "" {
+		nameParts = append(nameParts, meta.PlatformName)
+	}
+	return normalizeString(strings.Join(nameParts, "-"))
+}
+
+// normalizeString replaces slashes, dots, colons, percent signs and parentheses
+// with dashes, matching kedautil.NormalizeString.
+func normalizeString(s string) string {
+	replacer := strings.NewReplacer("/", "-", ".", "-", ":", "-", "%", "-", "(", "-", ")", "-")
+	return replacer.Replace(s)
+}
Evidence
The metric name builder appends PlatformName directly and the normalizer does not replace spaces;
the metadata test suite explicitly uses a platformName containing a space, showing this input is
supported.

.keda-external-scaler/internal/gridscaler/server.go[107-129]
.keda-external-scaler/internal/gridscaler/metadata_test.go[174-201]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`normalizeString` only replaces a small set of characters and does not handle spaces or casing, yet `buildMetricName` appends `PlatformName` verbatim and documents the result as "HPA-safe". Inputs like `platformName: "Windows 11"` will produce metric names with spaces.

## Issue Context
The repo's own tests demonstrate that platform names with spaces are expected metadata inputs.

## Fix Focus Areas
- .keda-external-scaler/internal/gridscaler/server.go[107-129]
- .keda-external-scaler/internal/gridscaler/metadata_test.go[174-201]

### Suggested fix approach
1. Implement a stricter normalization function (e.g., `strings.ToLower`, replace whitespace and any non `[a-z0-9-]` with `-`, collapse repeated `-`, trim leading/trailing `-`).
2. Add a server test case for `platformName: "Windows 11"` asserting the expected normalized metric name.
3. Ensure behavior stays aligned with the intended "HPA-safe" contract in the comments.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Unsafe type assertions 🐞 Bug ☼ Reliability
Description
Capability handling uses unchecked type assertions (string/bool) on JSON-decoded maps; malformed
trigger metadata or unexpected decoded types can panic the scaler instead of returning a gRPC status
error.
Code

.keda-external-scaler/internal/gridscaler/logic.go[R47-52]

+func getCapability(capability map[string]interface{}, key string) string {
+	value, ok := capability[key]
+	if ok {
+		return value.(string)
+	}
+	return ""
Evidence
The code asserts types without checking, and the module spec requires avoiding panics on malformed
metadata.

.keda-external-scaler/internal/gridscaler/logic.go[47-52]
.keda-external-scaler/internal/gridscaler/logic.go[92-103]
.keda-external-scaler/SPEC.md[219-223]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`getCapability` and `managedDownloadsEnabled` use direct type assertions (`.(string)`, `.(bool)`) on values produced by `encoding/json` unmarshalling. If inputs are malformed or unexpected, the scaler can panic.

## Issue Context
This is explicitly disallowed by the scaler module's spec: it should return gRPC status errors, not crash.

## Fix Focus Areas
- .keda-external-scaler/internal/gridscaler/logic.go[47-52]
- .keda-external-scaler/internal/gridscaler/logic.go[92-103]
- .keda-external-scaler/SPEC.md[219-223]

### Suggested fix approach
1. Replace type assertions with safe checks:
  - `v, ok := value.(string)` / `v, ok := value.(bool)`
2. Decide on deterministic fallback behavior when types are wrong (e.g., treat as non-match and continue) and ensure no panics.
3. Add tests for malformed capability values to confirm the scaler remains up and returns a gRPC error or yields a safe non-match outcome.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Invalid nodeMaxSessions allowed 🐞 Bug ≡ Correctness
Description
parseMetadata accepts nodeMaxSessions=0 or negative with no validation, but
getCountFromSeleniumResponse subtracts 1 and uses it as available slots, producing negative slot
counts and incorrect scaling decisions.
Code

.keda-external-scaler/internal/gridscaler/metadata.go[R112-118]

+	if v := lookup("nodeMaxSessions"); v != "" {
+		n, err := strconv.ParseInt(v, 10, 64)
+		if err != nil {
+			return nil, fmt.Errorf("parsing nodeMaxSessions: %w", err)
+		}
+		meta.NodeMaxSessions = n
+	}
Evidence
Metadata parsing allows any integer, while the core algorithm directly computes nodeMaxSessions-1,
which becomes negative for 0/negative inputs.

.keda-external-scaler/internal/gridscaler/metadata.go[112-118]
.keda-external-scaler/internal/gridscaler/logic.go[258-261]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`nodeMaxSessions` is parsed and assigned without bounds checks, but the scaling algorithm assumes it is positive (it uses `nodeMaxSessions-1` as a remaining-slot count). Zero/negative values lead to nonsensical slot reservations and incorrect node counts.

## Issue Context
This is trigger metadata coming from KEDA; invalid values should be rejected as `InvalidArgument` during metadata parsing.

## Fix Focus Areas
- .keda-external-scaler/internal/gridscaler/metadata.go[112-118]
- .keda-external-scaler/internal/gridscaler/logic.go[258-261]

### Suggested fix approach
1. In `parseMetadata`, after parsing, enforce `meta.NodeMaxSessions >= 1`; otherwise return an error.
2. Add a unit test asserting invalid `nodeMaxSessions` values fail metadata parsing (and therefore RPCs return `codes.InvalidArgument`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

5. Non-printable reserved node IDs 🐞 Bug ⚙ Maintainability
Description
New reserved node IDs are created via string(rune(requestIndex)), producing control/unprintable
characters for low indices (e.g., NUL for 0), which makes debugging and any future
logging/serialization confusing.
Code

.keda-external-scaler/internal/gridscaler/logic.go[R258-261]

+		// Check if a new Node should be scaled up to reserve for the matched request
+		if isRequestMatched && !isRequestReserved {
+			newRequestNodes = updateOrAddReservedNode(newRequestNodes, string(rune(requestIndex)), nodeMaxSessions-1, nodeMaxSessions)
+		}
Evidence
The code explicitly converts the integer index to a rune, which maps 0..31 to ASCII control
characters, creating unreadable IDs.

.keda-external-scaler/internal/gridscaler/logic.go[258-261]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`string(rune(requestIndex))` generates non-printable IDs for `ReservedNodes`. Even if the ID is currently only used as an internal key, it's an unnecessary footgun for debugging and future observability.

## Issue Context
The ID only needs to be unique within the function execution.

## Fix Focus Areas
- .keda-external-scaler/internal/gridscaler/logic.go[258-261]

### Suggested fix approach
Use `strconv.Itoa(requestIndex)` (or `fmt.Sprint(requestIndex)`) instead of `string(rune(requestIndex))` to generate a readable, stable identifier.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread .keda-external-scaler/internal/gridscaler/logic.go
Comment thread .keda-external-scaler/internal/gridscaler/server.go
Comment thread .keda-external-scaler/internal/gridscaler/logic.go
Comment thread .keda-external-scaler/internal/gridscaler/metadata.go
Comment thread .keda-external-scaler/internal/gridscaler/logic.go
@VietND96
VietND96 force-pushed the keda-external-scaler branch from be29ccf to de82b58 Compare July 11, 2026 17:59
Signed-off-by: Viet Nguyen Duc <nguyenducviet4496@gmail.com>
@VietND96
VietND96 force-pushed the keda-external-scaler branch from de82b58 to 92d7cbf Compare July 11, 2026 18:00
…unAsNonRoot

The scaler pod failed to start with CreateContainerConfigError because the
distroless image ran as the non-numeric user "nonroot" while the pod set
securityContext.runAsNonRoot: true without a numeric runAsUser. Kubernetes
cannot verify a name-based user is non-root and rejects the container.

Use the numeric distroless uid/gid (65532) in the Dockerfile USER, and set
runAsUser/runAsGroup: 65532 explicitly in the chart template and the
reference Deployment so the runAsNonRoot check passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VietND96
VietND96 force-pushed the keda-external-scaler branch 5 times, most recently from cc3a9ea to 3af6405 Compare July 12, 2026 18:18
…ons strategy

For scalingType=job, set the trigger metadata jobScalingStrategy to the
effective ScaledJob scalingStrategy.strategy (autoscaling.scaledJobOptions,
including per-node overrides). This keeps the scaler's count convention in
lockstep with the ScaledJob strategy KEDA applies, avoiding the accurate/eager
double-counting (#3167). Applies to both the external
and built-in selenium-grid scalers; omitted for deployment scaling. A value
set explicitly in node.hpa still takes precedence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Viet Nguyen Duc <nguyenducviet4496@gmail.com>
@VietND96
VietND96 force-pushed the keda-external-scaler branch from 3af6405 to 8dc5031 Compare July 12, 2026 18:25
…tegy

Make the external scaler the chart default (autoscaling.externalScaler.enabled:
true) and default the ScaledJob scalingStrategy.strategy to "accurate" (the
value the scaler needs to avoid accurate/eager double-counting, #3167).

Test script:
- default SCALING_STRATEGY to accurate to match the chart.
- pin autoscaling.externalScaler.enabled=${TEST_EXTERNAL_SCALER} deterministically
  so built-in strategies still exercise selenium-grid despite the new default,
  while job_externalScaler exercises the external path.
- chart_test_autoscaling_job_externalScaler sets TEST_EXTERNAL_SCALER=true so it
  works for local runs too, and is added to the render-template list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Viet Nguyen Duc <nguyenducviet4496@gmail.com>
@VietND96
VietND96 force-pushed the keda-external-scaler branch from f0b9bb0 to dfca4d7 Compare July 12, 2026 21:30
With the external scaler now the chart default, ScaledObject/ScaledJob triggers
use type: external with a scalerAddress and no authenticationRef, which broke
test_scaler_triggers_authenticationRef_name_is_added (KeyError: authenticationRef).

Rewrite it as test_scaler_triggers_auth_wiring: assert scalerAddress and absence
of authenticationRef for external triggers, and authenticationRef.name for the
built-in selenium-grid triggers. This passes for either default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Viet Nguyen Duc <nguyenducviet4496@gmail.com>
@VietND96
VietND96 force-pushed the keda-external-scaler branch from dfca4d7 to d0ed386 Compare July 12, 2026 23:02
@VietND96 VietND96 changed the title K8s: KEDA external scaler K8s: Implement KEDA external scaler and use it as default Jul 12, 2026
@VietND96
VietND96 merged commit d267e2a into trunk Jul 13, 2026
29 checks passed
@VietND96
VietND96 deleted the keda-external-scaler branch July 13, 2026 00:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant