Skip to content

Use kebab-case property keys for env-var-overridable config properties - #1096

Merged
gavinelder merged 3 commits into
masterfrom
chiusole/add-explicit-env-vars-for-camel-case-mn-properties
Jul 23, 2026
Merged

Use kebab-case property keys for env-var-overridable config properties#1096
gavinelder merged 3 commits into
masterfrom
chiusole/add-explicit-env-vars-for-camel-case-mn-properties

Conversation

@bebosudo

@bebosudo bebosudo commented Jul 20, 2026

Copy link
Copy Markdown
Member

Problem

Several Wave config properties use a camelCase segment in their name — e.g. wave.build.k8s.storage.claimName, wave.blobCache.storage.accessKey, wave.httpclient.connectTimeout. Such a property cannot be populated from an environment variable via Micronaut's implicit ENV_VAR fold: the uppercased env var loses the word boundary, so WAVE_BUILD_K8S_STORAGE_CLAIMNAME folds to the candidate claimname, which never matches the property's normalized key claim-name. The value silently binds to null and can only be set from a config file, not an env var.

This was hit in practice: WAVE_BUILD_K8S_STORAGE_CLAIMNAME / ..._MOUNTPATH had no effect, and storageClaimName / storageMountPath logged as null.

Fix

Switch the affected @Value / @Property (and the corresponding @Requires) keys to kebab-case. Micronaut resolves a kebab key against both the kebab and the legacy camelCase spelling, so:

  • Existing config files keep workingclaimName / mountPath / blobCache.* still bind unchanged (kebab reads camelCase).
  • The env var now works everywhere — the kebab key folds correctly from WAVE_BUILD_K8S_STORAGE_CLAIM_NAME, WAVE_BLOB_CACHE_STORAGE_ACCESS_KEY, WAVE_HTTPCLIENT_CONNECT_TIMEOUT, …
  • If both spellings are set in the same config file, the kebab value wins.

Why at the @Value source, not in application.yml

For the wave.build.k8s.* keys, declaring any such value in the shared base config would satisfy the @Requires(property = 'wave.build.k8s') gate on K8sServiceImpl (and the other Kube* beans) in every environment, forcing them to instantiate where wave.build.k8s.namespace is not set (this broke 121 tests in an earlier attempt). Keeping the change in the annotation avoids tripping the gate. The empty default on the nullable k8s storage fields keeps them optional without introducing the property into shared config.

Keys converted (property → env var)

Property (kebab) Environment variable
wave.build.k8s.storage.claim-name WAVE_BUILD_K8S_STORAGE_CLAIM_NAME
wave.build.k8s.storage.mount-path WAVE_BUILD_K8S_STORAGE_MOUNT_PATH
wave.build.k8s.config-path WAVE_BUILD_K8S_CONFIG_PATH
wave.build.logs.max-length WAVE_BUILD_LOGS_MAX_LENGTH
wave.allow-anonymous WAVE_ALLOW_ANONYMOUS
wave.deny-paths / wave.deny-hosts WAVE_DENY_PATHS / WAVE_DENY_HOSTS
wave.close-session-on-invalid-license-token WAVE_CLOSE_SESSION_ON_INVALID_LICENSE_TOKEN
wave.pairing.channel.await-timeout / max-attempts / retry-back-off-base / retry-back-off-delay / retry-max-delay WAVE_PAIRING_CHANNEL_*
wave.httpclient.connect-timeout / stream-threshold / retry.max-delay WAVE_HTTPCLIENT_*
wave.aws.sts.retry.max-delay WAVE_AWS_STS_RETRY_MAX_DELAY
wave.mirror.skopeo-image WAVE_MIRROR_SKOPEO_IMAGE
wave.cache.digest-store.max-weight-mb WAVE_CACHE_DIGEST_STORE_MAX_WEIGHT_MB
wave.blob-cache.* (status, storage.access-key/secret-key/endpoint, base-url, s5cmd-image, k8s.resources., cloudflare.) WAVE_BLOB_CACHE_*

Compatibility

No config contract is broken: the camelCase spelling remains an accepted alias, so existing application.yml / config.yml deployments and the docs' examples continue to work. The kebab spelling is simply also accepted and is the form that folds from env vars.

Verification

  • Verified against Micronaut 4.10 that a kebab @Value key resolves both spellings, that empty-string defaults coerce to null for typed scalars, that a bare ${VAR} with no default fails startup (avoided), and that an empty @Value default does not trip a @Requires(property=...) gate.
  • Full test suite: no config-binding failures introduced. The remaining failures in a local run are pre-existing credential/network-dependent tests (RegistryAuthServiceTest etc.), confirmed to fail identically on master.

🤖 Generated with Claude Code

@bebosudo
bebosudo marked this pull request as draft July 20, 2026 20:54
@bebosudo
bebosudo force-pushed the chiusole/add-explicit-env-vars-for-camel-case-mn-properties branch from d0e5bc2 to 8c6a75c Compare July 21, 2026 08:50
@bebosudo bebosudo changed the title Add explicit env-var placeholders for camelCase config properties Use kebab-case property keys for env-var-overridable config properties Jul 21, 2026
@bebosudo
bebosudo marked this pull request as ready for review July 21, 2026 10:51
private boolean debug

@Value('${wave.build.k8s.storage.claimName}')
@Value('${wave.build.k8s.storage.claim-name:}')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am not sure we want to keep this empty defaults instead of using null when not set. That might be beyond the scope of this PR though

@pditommaso

Copy link
Copy Markdown
Collaborator

The kebab-case rename itself is safe — Micronaut normalizes every property-source key to kebab (NameUtils.hyphenate), so camelCase config files keep binding unchanged and this is a pure alias swap. Three things to address before merge:

1. Revert the behavior change in K8sServiceImpl. Everywhere else this PR only renames the key, but here it also adds an empty default:

@Value('${wave.build.k8s.storage.claim-name:}')   // ':' added
@Value('${wave.build.k8s.storage.mount-path:}')

The : changes the unset binding from null to "". It happens to be harmless today (all call sites use Groovy truthiness and every deployed env sets both values), but it's an unrelated semantic change in a "rename only" PR — and the default is redundant, since @Nullable with no default already binds null when the property is absent (exactly as the original claimName/mountPath did). Please drop the : and keep these two fields @Nullable with no default, matching all the other converted keys.

2. Revert the release bump. The VERSION1.36.1 and the changelog.txt entry shouldn't ride along in this config fix — let the release be cut separately per the normal release process. Please drop those two changes from this PR.

3. Follow-up: align the wave config in platform-deployment. Not required (camelCase still binds), but for consistency we should also switch the mounted config.yml to kebab in applications/wave/environments/{tower-dev,tower-stage,tower-prod}/app.yml:

  • blobCacheblob-cache (incl. storage.accessKey/secretKeyaccess-key/secret-key, baseUrlbase-url)
  • denyPaths / denyHostsdeny-paths / deny-hosts
  • build.k8s.storage.mountPath / claimNamemount-path / claim-name

I'll open that as a separate platform-deployment PR alongside the version bump.

@gavinelder

Copy link
Copy Markdown
Contributor

if you can fix up the small nits around empty defaults we can include this in the changelog.

I think we should update the documentation as we cannot promise MN5/6 will keep this behaviour in the longterm.

Several Wave config properties used a camelCase segment in their name
(e.g. wave.build.k8s.storage.claimName, wave.blobCache.storage.accessKey,
wave.httpclient.connectTimeout). Such a property cannot be populated from
an environment variable via Micronaut's implicit ENV_VAR fold: the
uppercased env var loses the word boundary, so WAVE_..._CLAIMNAME folds
to the candidate `claimname`, which never matches the property's
normalized key `claim-name`. As a result the value silently bound to
null and could only be set from a config file, not an env var.

Switch the @value / @Property (and the corresponding @requires) keys to
kebab-case. Micronaut normalizes every property-source key to kebab
(NameUtils.hyphenate), so this is a pure alias swap: existing config
using `claimName` / `mountPath` / `blobCache.*` keeps binding unchanged,
while the kebab key also folds correctly from the matching environment
variable (WAVE_BUILD_K8S_STORAGE_CLAIM_NAME,
WAVE_BLOB_CACHE_STORAGE_ACCESS_KEY, WAVE_HTTPCLIENT_CONNECT_TIMEOUT, ...).

The change is kept in the @value annotations rather than declared in the
shared application.yml: for the k8s.* keys, declaring any wave.build.k8s.*
value in the base config would satisfy the @requires(property =
'wave.build.k8s') gate on K8sServiceImpl (and the other Kube* beans) in
every environment, forcing them to instantiate where
wave.build.k8s.namespace is not set.

Keys converted (property -> env var):
- wave.build.k8s.storage.claim-name    WAVE_BUILD_K8S_STORAGE_CLAIM_NAME
- wave.build.k8s.storage.mount-path     WAVE_BUILD_K8S_STORAGE_MOUNT_PATH
- wave.build.k8s.config-path            WAVE_BUILD_K8S_CONFIG_PATH
- wave.build.logs.max-length            WAVE_BUILD_LOGS_MAX_LENGTH
- wave.allow-anonymous                  WAVE_ALLOW_ANONYMOUS
- wave.deny-paths / wave.deny-hosts     WAVE_DENY_PATHS / WAVE_DENY_HOSTS
- wave.close-session-on-invalid-license-token
- wave.pairing.channel.await-timeout / max-attempts /
  retry-back-off-base / retry-back-off-delay / retry-max-delay
- wave.httpclient.connect-timeout / stream-threshold / retry.max-delay
- wave.aws.sts.retry.max-delay
- wave.mirror.skopeo-image
- wave.cache.digest-store.max-weight-mb
- wave.blob-cache.* (status, storage.access-key/secret-key/endpoint,
  base-url, s5cmd-image, k8s.resources.*, cloudflare.*)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@bebosudo
bebosudo force-pushed the chiusole/add-explicit-env-vars-for-camel-case-mn-properties branch from 8c6a75c to 2d6ba3a Compare July 22, 2026 06:43

@pditommaso pditommaso left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @bebosudo, this looks great now — the earlier nits are all sorted and the rename is a clean kebab-case alias swap. Approving! 🎉

Leaving the actual merge to @gavinelder, since his docs request (we can't promise MN5/6 keeps this fold behaviour long-term) looks still unresolved.

@gavinelder
gavinelder merged commit c6d89f5 into master Jul 23, 2026
3 checks passed
@gavinelder
gavinelder deleted the chiusole/add-explicit-env-vars-for-camel-case-mn-properties branch July 23, 2026 15:11
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.

4 participants