[FLINK-39791][helm] Honor defaultConfiguration.config.yaml and always mount config.yaml#1126
[FLINK-39791][helm] Honor defaultConfiguration.config.yaml and always mount config.yaml#1126gerkElznik wants to merge 2 commits into
Conversation
| {{- if .Values.watchNamespaces }} | ||
| kubernetes.operator.watched.namespaces: {{ join "," .Values.watchNamespaces }} | ||
| {{- end }} | ||
| {{- if index .Values "operatorHealth" }} | ||
| kubernetes.operator.health.probe.enabled: true | ||
| kubernetes.operator.health.probe.port: {{ .Values.operatorHealth.port }} | ||
| {{- end }} | ||
| flink-conf.yaml: |+ | ||
| {{- if .Values.defaultConfiguration.append }} | ||
| {{- $.Files.Get "conf/flink-conf.yaml" | nindent 4 -}} | ||
| {{- end }} |
There was a problem hiding this comment.
Good question — there are three separate edits in that hunk:
-
The
watchNamespaces/operatorHealthinjections aren't actually dropped.
The old template emitted two config-file keys (config.yamland
flink-conf.yaml) and appended both blocks to each, so they rendered twice.
Collapsing to a singleconfig.yamlkey removes the duplicate — one copy still
renders just below the resolver (lines 37–43);helm templateconfirms
kubernetes.operator.health.probe.*and thewatched.namespacesline still
emit, once. -
The
hasKey "config.yaml"block is replaced by thewith-based resolver
above it. That's the core of the ticket:hasKeykeys off presence, and since
the chart hardcodes a non-emptydefaultConfiguration.flink-conf.yamlthat a
downstream-f values.yamlcan't unset, theconfig.yamlbranch was
unreachable.withkeys off value-truthiness (like the existinglog4j/
logbackblocks), so the user'sconfig.yamlactually wins. -
The
flink-conf.yaml:key is removed so the ConfigMap emits a single,
always-config.yamlfile (the only name Flink 2.x reads), mounted statically.
The one behavioral consequence of (3) I'd like your read on before acting: runtime
patching of the operator ConfigMap now has to target the config.yaml key. In
particular e2e-tests/test_dynamic_flink_conf.sh patches the flink-conf.yaml
key and would no longer be picked up — its twin test_dynamic_config.sh already
covers the same dynamic-reload path via config.yaml. I'm inclined to drop
test_dynamic_flink_conf.sh (and its two ci.yml matrix rows) plus add a short
migration note, but since that removes legacy coverage I'd rather confirm with you
first.
(Aside, unrelated to this hunk: I've rebased onto latest main — the earlier
test_application_operations.sh smoke failure was a pipeline.jars/Flink 1.20.4
app-mode error on a stale base.)
… mount config.yaml The operator Helm chart rendered both a `config.yaml` and a `flink-conf.yaml` entry into the operator ConfigMap and picked which one to mount with `hasKey`. A user's `defaultConfiguration.config.yaml` was therefore silently ignored, and the operator always mounted `flink-conf.yaml` assembled from the chart defaults. Render a single `config.yaml` ConfigMap entry, resolved as `defaultConfiguration.config.yaml` when set, otherwise the legacy `flink-conf.yaml`, otherwise the chart default, and always mount it as `config.yaml` — the only file name Flink 2.x reads. Rename the chart's seed `conf/flink-conf.yaml` to `conf/config.yaml` and drop its `taskmanager.numberOfTaskSlots` and `parallelism.default` entries: both already equal Flink's compiled defaults, `parallelism.default` is managed per-deployment by the operator, and removing them lets a flat user override of those keys avoid a duplicate-key collision under Flink 2.x's strict YAML parser. This makes `config.yaml` win over `flink-conf.yaml` and reduces the ConfigMap to a single key. The values.yaml comments are restructured into two mutually exclusive format examples, the English docs are updated, and the helm-unittest suite covers the resolver precedence, the single-key shape, and the dropped-seed-key behavior. Generated-by: Claude Code (Claude Opus 4.8) Co-Authored-By: Claude <noreply@anthropic.com>
|
@gerkElznik the e2es seem to be failing |
Heads-up before going further: this regresses Flink 1.x application deploymentsWhile tracking down the failing SymptomA Isolated with an A/BSame operator image (built from this branch), same kind cluster, same
Root cause
// … otherwise it would simply inherit it from the base config (which would always be false currently).
Configuration clusterSideConfig = new Configuration(useStandardYamlConfig()); // false for v1_x → flink-conf.yaml ✓
clusterSideConfig.addAll(flinkConfig); // copies the operator's global-config values
return ConfigurationUtils.convertConfigToWritableLines(clusterSideConfig, false);useStandardYamlConfig() correctly keys off the deployment's FlinkVersion, but addAll(flinkConfig) copies values from the operator's global config — and the comment assumes that global config is "always false" (legacy). Once the operator reads config.yaml, its global config is standardYaml=true, so list options (pipeline.jars, pipeline.classpaths, …) carry standard-YAML form and get written into the v1_x flink-conf.yaml as ['local://…'], which the legacy parser rejects. So it's a pre-existing operator assumption that this PR is simply the first to expose — and it affects any list-valued option on any Flink-1.x deployment, not only pipeline.jars. How would you like to sequence this @gyfora? Tagging @Dennis-Mircea as well since he's mentioned planning for the overall 2.x upgrade.
Happy to help with either, including the operator-side code change. Two smaller notes, same root (single config.yaml key): |
|
@gyfora Following up on this: I have a fix ready on a branch, it keeps typed values intact through Since the scope of this original change has expanded into more complexity I took the liberty to open the fix PR for FLINK-40055 so it's reviewable but happy to pivot on your direction... if it's accepted I'll rebase this PR once it lands. |
|
The PR to land before this for the fix: #1152 |
Dennis-Mircea
left a comment
There was a problem hiding this comment.
Thanks for opening this PR! Good investigation on the process-global YAML dialect leak, and thanks for opening #1152 with the root cause and the verification matrix.
On sequencing, I'd support landing #1152 first and then reshaping this PR around a broader fix rather than the flat-seed workaround. The root cause of the collision surface (defaults duplicated across values.yaml + conf/config.yaml, plus text-written Helm-value injections) is the same issue Note 4 gestures at, and I think it deserves being fixed properly rather than mitigated.
Concretely, the cleanup I have in mind:
- Chart drops the SLF4J reporter class+interval,
reconcile.interval, andobserver.progress-check.intervaloverrides from values.yaml:- Reconcile and observer overrides fall back to their compiled
ConfigOptiondefaults (60 s and 10 s today, worth confirming those are the right values) which currently are not used at all. Maybe it makes sense to change those defaults with the overrided ones. - The SLF4J reporter class+interval can be dropped entirely. It is the user choice if it should be configured or not.
- Reconcile and observer overrides fall back to their compiled
- Chart's
conf/config.yaml(the Java 17 opts underkubernetes.operator.default-configuration.flink-version.*) migrates to compiled defaults on the operator side.conf/config.yamlbecomes empty, or a commented example that documents structure without shipping active values. watchNamespacesandoperatorHealthstop being text-written into the operator ConfigMap. They become env vars the operator reads at startup, mirroring howWATCH_NAMESPACESalready works today (Helm sets the env var,FlinkOperatorConfiguration.fromConfigurationreads it and overrides the runtimeConfiguration). Extending the same pattern toKUBERNETES_OPERATOR_HEALTH_PROBE_ENABLEDandKUBERNETES_OPERATOR_HEALTH_PROBE_PORTis symmetry, not new machinery.
Net result: config.yaml on disk contains only user-supplied content. No seed to collide against, no mixed-form aesthetic concern, no ordering-per-format debate. Note 4's residual surface goes away because there is no shipped-in-text seed left for a user override to duplicate.
Are you ok with this approach, @gyfora?
| # reconcile.interval: 15 s | ||
| # observer.progress-check.interval: 5 s | ||
| # taskmanager: | ||
| # numberOfTaskSlots: 1 # default for managed clusters; override per-job via spec.flinkConfiguration |
There was a problem hiding this comment.
Not necessary to specify this property here.
| path: log4j-operator.properties | ||
|
|
||
| # FLINK-39791: the operator config is always mounted as config.yaml (the only filename Flink 2.x | ||
| # reads), regardless of which defaultConfiguration key the user populates. |
| path: data["log4j-console.properties"] | ||
|
|
||
| # FLINK-39791: the ConfigMap emits a single config-file key, always named config.yaml, | ||
| # resolved as config.yaml (when set) > user flink-conf.yaml > the values.yaml default. |
| |------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ||
| | defaultConfiguration.append | Whether to append configuration files with configs. | true | | ||
| | defaultConfiguration.config.yaml | The newer configuration file format for flink that will enforced in Flink 2.0. Note this was introduced in flink 1.19. | kubernetes.operator.metrics.reporter.slf4j.factory.class: org.apache.flink.metrics.slf4j.Slf4jReporterFactory<br/>kubernetes.operator.metrics.reporter.slf4j.interval: 5 MINUTE<br/>kubernetes.operator.reconcile.interval: 15 s<br/>kubernetes.operator.observer.progress-check.interval: 5 s | | ||
| | defaultConfiguration.config.yaml | Modern YAML 1.2 config format (introduced in Flink 1.19, required by Flink 2.0). Takes precedence over flink-conf.yaml when set; the resolved config is always mounted as config.yaml. | kubernetes.operator.metrics.reporter.slf4j.factory.class: org.apache.flink.metrics.slf4j.Slf4jReporterFactory<br/>kubernetes.operator.metrics.reporter.slf4j.interval: 5 MINUTE<br/>kubernetes.operator.reconcile.interval: 15 s<br/>kubernetes.operator.observer.progress-check.interval: 5 s | |
There was a problem hiding this comment.
The chinese docs can be synced with the same english content.
Drop narrative test comments and the values.yaml taskmanager example; sync the Chinese docs with the same English content. Generated-by: Claude Code (Claude Opus 4.8) Co-Authored-By: Claude <noreply@anthropic.com>
|
Thanks @Dennis-Mircea! Inline comments are all addressed in the latest commits on both PRs (including the Chinese docs sync). Before responding to the cleanup proposal I tried each piece in a kind cluster and traced the code paths, to check my own understanding. Taking your three items in order: 1. Dropping the SLF4J reporter and interval overrides from values.yaml Confirmed the compiled defaults are 60s ( One consistency note: dropping the SLF4J reporter is also a behavior change on default installs, since operator metrics stop being logged every 5 minutes and anyone scraping or alerting on those lines would notice. I agree it should be user choice going forward; I'd just put it in the same release note. 2. Migrating the Java-17 opts to compiled defaults I think this is the right destination, and it's the same end-state note 4 above was gesturing at. Two design questions the follow-up would need to answer, since the per-version keys are dynamic string prefixes resolved at runtime (
3. watchNamespaces and operatorHealth as env vars
For the health probe: nothing reads On sequencing Landing #1152 first makes sense to me regardless. For this PR, my thinking is that the cleanup and the fix here are complementary rather than alternatives: the cleanup removes the collision surface, but even with an empty seed and env vars the chart still needs this PR's template changes to emit and mount the user's |
|
Quick follow-up: two points in my comment above were from code tracing only, so I runtime-tested both in the same kind setup (webhook enabled via cert-manager, namespaced RBAC via
Neither changes the options above; just confirming the details for the follow-up work. |
What is the purpose of the change
The Helm chart advertises
defaultConfiguration.config.yaml(added in FLINK-38409), but a downstream consumer who sets it never gets it mounted — the operator silently runs onflink-conf.yamlbuilt from chart defaults, while the user'sconfig.yamlcontent lands in theflink-operator-configConfigMap but is never read by the operator JVM.Three interacting causes:
values.yamlhardcodes a non-emptydefaultConfiguration.flink-conf.yaml. Helm's additive merge means a downstream-f my-values.yamlcannot unset it, sohasKey "flink-conf.yaml"is always true.templates/controller/configmap.yamlunconditionally emits bothconfig.yamlandflink-conf.yamlkeys.templates/controller/deployment.yamlselects the mounted file with an independenthasKey "flink-conf.yaml"— always true — so theconfig.yamlbranch is unreachable.This PR resolves the file choice in the chart template and emits/mounts a single, statically named
config.yaml. The approach was discussed and agreed on the JIRA ticket.Why one always-
config.yamlfile, appended from a flat seedThe chart seed carries the mandatory Java-17
--add-opens/--add-exportsopts, which must apply no matter which format the user supplies. So the seed is prepended to the resolved override — the same "append" modellog4j-*.propertiesalready uses — rather than replaced by it (the modellogback-*.xmluses, since two XML documents can't be concatenated). It cannot use the replace model, because that would drop the Java-17 opts for anyone who sets their ownconfig.yaml.Because the mounted result is parsed by Flink's strict
config.yamlparser (snakeyaml-engine,allowDuplicateKeys=false), an appended flat user override of a key that is also in the seed would be a duplicate-key crash. Two consequences, both reflected here:taskmanager.numberOfTaskSlots: 1/parallelism.default: 1are dropped — they only restate Flink's compiled default of1, so removing them is behavior-neutral while shrinking the collision surface.Brief change log
configmap.yaml— emit a single config-file key, always namedconfig.yaml, resolved with a nested{{ with (index … "config.yaml") }} … {{ else }}{{ with (index … "flink-conf.yaml") }} … {{ end }}{{ end }}(plainwith/else, so it renders on every Helm version): the user'sconfig.yamlwins, else theirflink-conf.yaml, else thevalues.yamldefault. Keys off value truthiness rather thanhasKey(which downstream can't unset), consistent with the chart's existinglog4j/logbackblocks.deployment.yaml— mount the key statically atpath: config.yaml. Emit and mount are both literallyconfig.yaml, so they can't drift;config.yamlis also the only filename Flink 2.x reads, so default installs keep booting after the operator's eventual 2.x rebase.conf/flink-conf.yaml→conf/config.yaml(renamed) — the always-prepended seed, named to match what it is mounted as; kept flat; the two redundant: 1keys removed (see above).values.yaml— restructured the showcase comment into two mutually-exclusive format examples (modernconfig.yaml/ legacyflink-conf.yaml) with precedence + mount notes; opinionated defaults stay here (not relocated into the always-applied seed).operations/configuration.md,operations/helm.md,development/guide.md) — document theconfig.yamlformat and precedence, and fix the now-stale/opt/flink/conf/flink-conf.yamlinspect path.Verifying this change
This change added tests and can be verified as follows:
tests/controller/{configmap,deployment}_test.yaml): exactly one config-file key, alwaysconfig.yaml(neverflink-conf.yaml), carrying today's effective defaults with the two dropped seed keys absent; a userflink-conf.yamlstill flows in whenconfig.yamlis unset;config.yamlwins when both are set; and a flat override of a former seed key no longer duplicates it. The Deployment mounts the key statically asconfig.yaml. (Values are injected via structuredset:; a dotted-stringset:no-ops on keys containing dots.)config.yamlwas loaded through a strict, no-duplicate-key YAML parser equivalent to Flink'sconfig.yamlloader, with a positive control confirming the check rejects a genuine duplicate. All rows parse clean./opt/flink/conf/config.yamlvia Flink's standard YAML parser — with scalar, nested-map, and list values — that the ConfigMap carries a singleconfig.yamlkey, and that the operator reconciles FlinkDeployments with both flat and nestedflinkConfiguration.ct lintagainst the chart-testing image (Helm 3.16.4) confirms the resolver renders on older Helm too.helm lintand a fullhelm templaterender pass.1either way.Does this pull request potentially affect one of the following parts:
CustomResourceDescriptors): noDocumentation
defaultConfiguration.config.yamlwork as advertised.operations/configuration.mdandoperations/helm.md.Notes for reviewers (call-outs / open questions)
config.yamlnow wins; the oldhasKeybehavior preferredflink-conf.yaml. Worth a release note.flink-operator-confignow carries one config-file key instead of two — visible to anyone reading that ConfigMap directly.mainyet; if it lands first, I'll rebase and remove it.config.yamloverride of the two remainingkubernetes.operator.default-configuration.flink-version.*Java-17 seed keys could still duplicate-key. It's rare (they're rarely flat-overridden, and the recommended nested form is collision-free). The clean long-term fix is shipping framework defaults as compiledConfigOptiondefaults rather than a text-prepended seed — proposed as a separate follow-up.release-*branch(es) should this target?