Skip to content

feat: add EnvoyProxy.spec.runtime served over RTDS - #9671

Draft
kanurag94 wants to merge 1 commit into
envoyproxy:mainfrom
kanurag94:feat/envoyproxy-spec-runtime
Draft

feat: add EnvoyProxy.spec.runtime served over RTDS#9671
kanurag94 wants to merge 1 commit into
envoyproxy:mainfrom
kanurag94:feat/envoyproxy-spec-runtime

Conversation

@kanurag94

@kanurag94 kanurag94 commented Aug 5, 2026

Copy link
Copy Markdown
Member

What this PR does / why we need it:

Adds EnvoyProxy.spec.runtime, a map of Envoy runtime key to value, served to proxies over RTDS.

Today there is no API for Envoy runtime values. The only path to layered_runtime is spec.bootstrap, and the bootstrap is read at startup, so every change restarts the proxy. The default bootstrap has one static layer (internal/xds/bootstrap/bootstrap.yaml.tpl:50) and no RTDS layer.

The RTDS server is already wired but unreachable. internal/xds/runner/runner.go:266 registers it:

runtimev3.RegisterRuntimeDiscoveryServiceServer(g, srv)

grep -rn RuntimeType internal/ on main returns nothing — no code ever emits a Runtime resource, so that registration is dead.

What a user cannot do: tune circuit breakers on a cluster Envoy Gateway did not build from its own configuration. BackendTrafficPolicy circuit breakers reach only IR-derived clusters, applied at internal/xds/translator/cluster.go:495. A cluster an extension server appends in PostTranslateModify is not IR-derived, so it keeps Envoy's defaults, upstream_impl.cc:2199-2202:

uint64_t max_connections = 1024;
uint64_t max_pending_requests = 1024;
uint64_t max_requests = 1024;
uint64_t max_retries = 3;

ext_proc opens one gRPC stream per HTTP request, so max_requests: 1024 caps a proxy at 1024 concurrent requests through ext_proc.

Editing the cluster to raise it is itself disruptive. A cluster update increments cluster_manager.cluster_modified and runs thread_local_clusters_[info->name()].reset(new_cluster) (cluster_manager_impl.cc:1325). That destroys the ClusterEntry, which owns lazy_http_async_client_ (cluster_manager_impl.h:716), and ~AsyncClientImpl resets every active stream (async_client_impl.cc:38-42):

AsyncClientImpl::~AsyncClientImpl() {
  while (!active_streams_.empty()) {
    active_streams_.front()->reset();
  }
}

ext_proc streams use the async client, not the connection pool, so drainConnPools() in the ClusterEntry destructor does not cover them.

Envoy reads the limit from runtime on every admission check, basic_resource_impl.h:

bool canCreate() override { return current_.load() < max(); }

uint64_t max() override {
  return (runtime_ != nullptr && runtime_key_.has_value())
             ? runtime_->snapshot().getInteger(runtime_key_.value(), max_)
             : max_;
}

So a runtime key applies with no circuit_breakers block on the cluster, and changing it modifies no cluster and resets no streams.

Example

Before — the default bootstrap on main has one static layer and no RTDS layer, so nothing can
reach runtime on a running proxy:

$ git show main:internal/xds/bootstrap/bootstrap.yaml.tpl | sed -n '/^layered_runtime:/,/^dynamic_resources:/p' | sed '$d'
layered_runtime:
  layers:
  - name: global_config
    static_layer:
      re2.max_program_size.error_level: 4294967295
      re2.max_program_size.warn_level: 1000

After — spec.runtime on the EnvoyProxy:

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
  name: custom-proxy-config
  namespace: envoy-gateway-system
spec:
  runtime:
    circuit_breakers.ai-gateway-extproc-uds.default.max_requests: 16384

The translator emits one Runtime resource. Golden output, internal/xds/translator/testdata/out/xds-ir/runtime.runtime.yaml:

- layer:
    circuit_breakers.ai-gateway-extproc-uds.default.max_requests: 16384
    envoy.reloadable_features.some_feature: false
    overload.global_downstream_max_connections: 50000
  name: envoy-gateway-runtime

Envoy resolves the key with no circuit_breakers block on the cluster. Run against
envoyproxy/envoy:distroless-dev with a static config whose slow_cluster carries no
circuit_breakers block, and a static layer setting the key to 1. Reading /runtime and
filtering to the circuit breaker entries:

LAYERS: ['global_config', 'admin_layer']
{
 "circuit_breakers.slow_cluster.default.max_requests": {
  "final_value": "1",
  "layer_values": [
   "1",
   ""
  ]
 }
}

final_value resolves from the static layer with nothing on the cluster, and the empty second
layer_values entry is the admin layer, which would override it.

Values keep their JSON type:

spec:
  runtime:
    circuit_breakers.my-cluster.default.max_requests: 16384      # number
    envoy.reloadable_features.some_feature: false                # boolean
    some.string.key: "a string"                                  # string

Which issue(s) this PR fixes:

Fixes #9670

Related issues/PRs

No prior PR has served RTDS from Envoy Gateway.

Notes for reviewers

  • Values are apiextensionsv1.JSON, not strings (api/v1alpha1/envoyproxy_types.go:95). Envoy's createEntry sends a string "true" through parseEntryDoubleValue, which fails, leaving bool_value_ unset, so getBoolean returns the default. A map[string]string would silently disable every boolean runtime guard. Cost is x-kubernetes-preserve-unknown-fields: true on the map values. Happy to switch to map[string]string if you would rather not have freeform JSON, but boolean keys stop working.

  • The layer is envoy-gateway-runtime, not runtime-0 (internal/xds/translator/runtime.go:27). Envoy refuses to start on duplicate layer names. runtime-0 was Envoy Gateway's own name before bootstrap: clean layered runtime #2051, so it can still be sitting in a user's spec.bootstrap, and reusing it would crashloop them. Verified against the pinned image:

    [1][critical][main] [source/server/config_validation/server.cc:76] error initializing
    configuration '': Duplicate layer name: envoy-gateway-runtime
    
  • Bootstrap validation now rejects duplicate runtime layer names. Without it that collision only shows up as a crashloop, since it is an Envoy startup check and not a PGV constraint, so patchedBootstrap.Validate() does not catch it.

  • testdata/merge/default.in.yaml already contained an rtds_layer named runtime-0. That fixture is what surfaced the collision. It is unchanged, and merge/default.out.yaml now shows a user layer coexisting with Envoy Gateway's.

  • The layer is listed after global_config, so runtime values win. SnapshotImpl::SnapshotImpl iterates layers in order doing values_.erase(kv.first) then emplace, so later layers replace earlier ones.

  • An unparseable value fails translation, which holds the whole snapshot (runner.go:335 logs skipped publishing xds resources). The API server only admits valid JSON here so this is close to unreachable, and a runtime key that silently fails to apply is worse — a dropped circuit breaker limit leaves the proxy on Envoy's default with nothing to show it. Happy to make it non-fatal like processJSONPatches if you prefer partial application.

  • spec.bootstrap with type: Replace that omits the RTDS layer leaves spec.runtime with nowhere to land. Documented on the field and in the task docs, not detected. Happy to add an EnvoyProxy status condition in this PR if you want it surfaced.

  • Adding the layer changes proxy container arguments, so upgrading rolls managed proxies once. Called out in release-notes/current/other_changes/9670-bootstrap-rtds-layer.md.

  • spec.extraArgs cannot be used for this. internal/infrastructure/common/proxy_args.go:71 already passes the whole bootstrap as --config-yaml, and ExtraArgs is appended at :98, so a second one is rejected:

    (--config-yaml) -- Argument already set!
    
  • egctl x translate cannot show these values. Envoy's config dump has no runtime section — the messages are Bootstrap, Secrets, Listeners, Clusters, Routes, ScopedRoutes, Endpoints, Ecds. It shows the RTDS layer in the bootstrap only. The task docs point at the proxy's /runtime endpoint instead.

  • Existing xDS golden files did not change. TestTranslateXds compares listeners, routes, clusters, endpoints and secrets, so the new type does not touch them. The .runtime.yaml assertion is gated on len(x.Runtime) > 0 to avoid adding a golden file to every case. The 83 regenerated files are all the bootstrap layer.

  • xdsWithoutEqual in internal/gatewayapi/translator_test.go needed the new field. It panics on a field missing from its ir.Xds mirror, which is deliberate.

  • No e2e test for live RTDS delivery. That needs a cluster. Everything below it is covered; the delivery path itself is not. Can add one in a follow-up.

Verification

Reviewed against the Envoy this repo pins, envoyproxy/envoy:distroless-dev, 1.40.0-dev/6dc58a7029680bb971116fde197d51e5439c776f:

  • Runtime prefix circuit_breakers.{cluster}.{priority}. built in ClusterInfoImpl::ResourceManagers::load, key suffixes in resource_manager_impl.h, priority names default and high.
  • Rendered bootstrap loads. --mode validate gets past layered_runtime and all 3 clusters, failing only on SDS paths absent from the container: paths must refer to an existing path in the system: '/sds/xds-certificate.json' does not exist.

Ran:

  • go build ./...
  • go test ./... — all packages pass
  • make lint0 issues.
  • make generate manifests helm-template protos go.testdata.complete twice — second run produces no diff

Each new test fails without the change:

  • Drop the processRuntime call from translator.goTestTranslateXds/runtime: Expected has the layer, Actual is +[].
  • Revert validate.goTestValidateBootstrap/user_bootstrap_with_duplicate_runtime_layer_names: An error is expected but got nil.
  • Drop xdsIR[irKey].Runtime = ... from gatewayapi/translator.goTestTranslate/envoyproxy-runtime diffs on Runtime map[string]v1.JSON.
  • Revert bootstrap.yaml.tplTestGetRenderedBootstrapConfig diffs on the six missing envoy-gateway-runtime lines.

PR Checklist

  • Authorship & ownership: Coding agents / AI assistants are welcome, but I have reviewed every change, understand how and why it works, can explain and maintain it, and take full responsibility for this PR. I have not submitted generated output I do not understand.
  • DCO: All commits are signed off (git commit -s).
  • API agreed first: Add EnvoyProxy.spec.runtime to configure Envoy runtime values, served over RTDS #9670 was filed ahead of the implementation, but the API has not been agreed yet — it is still labelled triage. Happy to split api/v1alpha1/envoyproxy_types.go into its own PR and hold this one behind it.
  • Required checks pass: go build ./..., go test ./..., and make lint (0 issues.) pass locally. gen-check requires a clean tree so it cannot run against uncommitted work; verified instead by running make generate manifests helm-template protos go.testdata.complete twice and confirming the second run produces no diff. CI is the authority.
  • Tests added/updated: unit tests per JSON value type and for the always-emitted empty resource, a runtime xDS translator case with golden output, an envoyproxy-runtime gatewayapi case for the EnvoyProxy to IR plumbing, and a bootstrap validation case for the duplicate layer name. Each verified to fail without the change (see Verification).
  • Docs: site/content/en/latest/tasks/operations/customize-envoyproxy.md, plus generated api/extension_types.md.
  • Release notes: release-notes/current/new_features/9670-envoyproxy-spec-runtime.md and release-notes/current/other_changes/9670-bootstrap-rtds-layer.md.
  • Generated files committed: CRDs, deepcopy, clients, helm templates and API docs regenerated and committed.
  • Scope & compatibility: no behaviour change unless spec.runtime is set. The one upgrade effect is a single proxy rollout from the bootstrap change, noted in other_changes.
  • Codex review: not yet requested.
  • Copilot review: not yet requested.

@kanurag94
kanurag94 requested a review from a team as a code owner August 5, 2026 04:29
@netlify

netlify Bot commented Aug 5, 2026

Copy link
Copy Markdown

Deploy Preview for cerulean-figolla-1f9435 ready!

Name Link
🔨 Latest commit 6b92d4a
🔍 Latest deploy log https://app.netlify.com/projects/cerulean-figolla-1f9435/deploys/6a734eca90b4570007a82010
😎 Deploy Preview https://deploy-preview-9671--cerulean-figolla-1f9435.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@kanurag94
kanurag94 marked this pull request as draft August 5, 2026 04:32
@kanurag94
kanurag94 force-pushed the feat/envoyproxy-spec-runtime branch from 6dc509f to dbf0ee3 Compare August 5, 2026 04:33

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6dc509fa14

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +59 to +61
rtds_config:
ads: {}
resource_api_version: V3

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep RTDS from timing out during bootstrap

When the initial RTDS response is delayed, this new rtds_config inherits Envoy's 15s initial_fetch_timeout default (ConfigSource docs), unlike the LDS/CDS config below that explicitly waits indefinitely. In that startup or controller-restart scenario Envoy can initialize this layer without the configured runtime values and run with default circuit-breaker/runtime behavior until a later RTDS update arrives, undermining the no-restart runtime path; please set initial_fetch_timeout: 0s here as well.

Useful? React with 👍 / 👎.

@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.28571% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.18%. Comparing base (aaa5569) to head (6b92d4a).

Files with missing lines Patch % Lines
internal/xds/translator/translator.go 0.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9671      +/-   ##
==========================================
- Coverage   76.19%   76.18%   -0.01%     
==========================================
  Files         261      262       +1     
  Lines       43528    43563      +35     
==========================================
+ Hits        33164    33189      +25     
- Misses       8161     8169       +8     
- Partials     2203     2205       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@kanurag94
kanurag94 force-pushed the feat/envoyproxy-spec-runtime branch from dbf0ee3 to db92be8 Compare August 5, 2026 05:00
Signed-off-by: Anurag Aggarwal <kanurag94@gmail.com>
@kanurag94
kanurag94 force-pushed the feat/envoyproxy-spec-runtime branch from db92be8 to 6b92d4a Compare August 5, 2026 14:55
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.

Add EnvoyProxy.spec.runtime to configure Envoy runtime values, served over RTDS

1 participant