refactor: adapt hdfs-operator to the rewritten operator-go framework - #319
Merged
Merged
Conversation
Migrate hdfs-operator from the imperative resource-building model (operator-go v0.12.6) to the rewritten declarative framework (GenericReconciler + RoleGroupHandler + ProductConfig). Phase 0-2 (compiling skeleton): - api/v1alpha1: HdfsCluster implements common.ClusterInterface; roles become plural (nameNodes/dataNodes/journalNodes) embedding the SDK RoleSpec; Status embeds GenericClusterStatus; Image uses the SDK ImageSpec. Regenerated deepcopy + CRD. - internal: new trino-style structure — constants, product.ComputeConfig (core-site/hdfs-site via the merge pipeline), and HdfsRoleGroupHandler embedding BaseRoleGroupHandler. - cmd/main.go: wired to the SDK GenericReconciler. - Removed the obsolete imperative internal/common and per-role builders. Dev-only: go.mod replaces operator-go with the local checkout; this must be dropped and a real tag pinned before merge. HA, ZKFC, init containers, discovery, Kerberos/TLS/OIDC, webhook and tests are reintroduced in later phases. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Expand ProductConfig from the skeleton's minimal config to the real HDFS
HA core-site.xml / hdfs-site.xml, ported from the old imperative
hdfs_conf.go but adapted to the SDK resource naming:
- core-site: fs.defaultFS -> the logical nameservice, ha.zookeeper.quorum
via ${env.ZOOKEEPER}.
- hdfs-site: dfs.nameservices, failover proxy provider, dfs.ha.namenodes
list, and per-NameNode rpc/http/name.dir addresses; the JournalNode
quorum shared-edits qjournal:// URI; automatic-failover/fencing keys.
Key correction vs the old code: the SDK headless Service is
{cluster}-{role}-{group}-headless (suffix), so pod FQDNs use that suffix;
copying the old naming would break NameNode/JournalNode DNS resolution.
Env-var references (POD_NAME/POD_ADDRESS/IPC_PORT/DATA_PORT/ZOOKEEPER)
are emitted here; the container env wiring lands in the next phase.
Adds table-driven tests asserting the HA addresses, the qjournal quorum
URI, and the DataNode data dir.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ase 4a)
Wire the primary container of each role's StatefulSet in BuildResources:
- env: HADOOP_HOME, HADOOP_CONF_DIR (= framework ConfigMountPath),
POD_NAME (fieldRef), and ZOOKEEPER (configMapKeyRef from the user's
zookeeperConfigMap) — the ${env.X} references the phase-3 config needs.
- command: exec ${HADOOP_HOME}/bin/hdfs <role>.
env/command are pure helpers (commonEnv, roleStartupCommand) with unit
tests. Also fix a latent double-slash in HadoopHome (KubedoopRoot already
ends with '/').
DataNode registration env (POD_ADDRESS/IPC_PORT/DATA_PORT), init
containers, ZKFC sidecar and Kerberos/TLS remain for phase 4b+.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Use the framework's declarative provisioners on the role group handler: - StorageMountPath = KubedoopDataDir so the SDK builds the data VolumeClaimTemplate (NameNode name.dir / JournalNode edits.dir / DataNode data.dir all live under it). - a per-pod listener CSI volume registered via listener.ListenerProvisioner and injected through the new BaseRoleGroupHandler.WithVolumeProvisioners hook (the operator-go VolumeProvisioner unification). The pod reads its externally reachable address from this mount. Dev-only: go.mod replace now points at the operator-go worktree carrying the VolumeProvisioner change until it lands on operator-go main/tag. Init containers (format-namenode/format-zk/wait-for-namenodes), the ZKFC sidecar and the POD_ADDRESS/IPC_PORT/DATA_PORT export script are the next 4b slice. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The framework merged a better design than the original WithVolumeProvisioners hook: RoleGroupBuildContext.VolumeProviders (per-build-context, per-role, rebuilt every reconcile) instead of a global per-handler registration. - Register the listener CSI volume via buildCtx.VolumeProviders in BuildResources (before delegating to the base handler), not on the handler in the constructor. - Point go.mod replace back at the operator-go main checkout, which now carries #510. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reintroduce the HDFS product-specific containers via the framework's
per-build-context SidecarManager (buildCtx.SidecarManager), injected as
StaticContainerProviders:
- NameNode: format-namenode (format or bootstrapStandby based on the
active-namenode probe + VERSION file), format-zookeeper (formatZK from
pod 0), and zkfc as a native sidecar (RestartPolicy=Always).
- DataNode: wait-for-namenodes (blocks on `hdfs haadmin -getServiceState`).
Main container command now exports POD_ADDRESS and <NAME>_PORT from the
listener mount (default-address/address + ports/*) before exec'ing the
daemon, so the config's ${env.POD_ADDRESS}/${env.IPC_PORT}/${env.DATA_PORT}
resolve. Container builders are pure functions with unit tests.
Scripts are simplified vs the old imperative code (config is mounted
directly, so no copy dance); Kerberos/TLS and vector shutdown coordination
are reintroduced in later phases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire HDFS TLS through the same VolumeProvider path as the listener volume:
- handler registers a PKCS12 TLS secret volume (security.TLS, gated on
spec.clusterConfig.authentication.tls) via buildCtx.VolumeProviders;
defaults mirror the CRD (secretClass "tls", password "changeit").
- ComputeConfig emits ssl-server.xml / ssl-client.xml pointing at the
keystore/truststore in the secret mount, and sets dfs.http.policy=
HTTPS_ONLY + dfs.https.{server,client}.keystore.resource in hdfs-site.
Pure-function unit tests cover the ssl config content and the gated
provisioner. Kerberos and OIDC remain for phases 5b/5c.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Kerberos, gated on spec.clusterConfig.authentication.kerberos:
- handler registers a per-role Kerberos secret volume (security.KerberosVolume
with the role service name nn/dn/jn + HTTP, service-scoped) through the same
buildCtx.VolumeProviders path as TLS/listener.
- commonEnv adds KRB5_CONFIG / KRB5_CLIENT_KTNAME / HADOOP_OPTS pointing at the
mounted krb5.conf / keytab; the main container script exports KERBEROS_REALM
(grepped from krb5.conf) so the ${env.KERBEROS_REALM} principals resolve.
- ComputeConfig emits the core-site principals (nn/dn/jn/HTTP + SPNEGO +
patterns), keytab locations, and the hdfs-site security keys (block access
tokens, keytab autorenewal, encrypted data transfer = privacy).
Pure-function unit tests cover the principals, keytab paths, env and gating.
kinit in the init containers (so format/wait can authenticate) is a follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…se 6) Reintroduce the discovery ConfigMap (named after the cluster) so external clients can reach the HA NameNodes: - internal/extensions/discovery_extension.go: a ClusterExtension whose PostReconcile renders product.DiscoveryConfig and applies the ConfigMap (owned by the cluster) via controllerutil.CreateOrUpdate. Runs once per cluster, after the role groups and their Services. - product.DiscoveryConfig returns the client-facing core-site/hdfs-site (fs.defaultFS, nameservice, failover proxy, per-NameNode rpc/http addresses, Kerberos client keys) — no pod-local keys. - Extract the shared nameNodeHAConfig helper used by both the pod hdfs-site.xml and the discovery config (removes duplication). - Register the extension in cmd/main.go. Pure-function test asserts the discovery content and that pod-local keys don't leak into it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…r-go #518) Sync onto operator-go main (now at #528) and adopt the shared discovery ensure-helper landed in #518: the DiscoveryExtension's PostReconcile now calls reconciler.EnsureDiscoveryConfigMap instead of hand-rolling controllerutil.CreateOrUpdate + SetControllerReference + labels. Same behaviour, less product code, canonical labels owned by the framework. Also picks up (no code change needed): #517 role group affinity / gracefulShutdownTimeout consumed by the base handler, and the #527/#528 reconciler fixes — hdfs builds/tests green against them. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gging Now that operator-go #531 landed per-role MainContainerName / LoggingContainers, wire HDFS logging through the framework: - name each role's primary container after the role (namenode / datanode / journalnode) via SetRoleMainContainerName, matching the kubedoop convention and the CRD logging keys (logging.containers.namenode, ...). - declare per-role log4j logging via SetRoleLoggingContainers; the SDK renders log4j.properties from the merged CRD logging spec into the role group ConfigMap (mounted at HADOOP_CONF_DIR) and, when the Vector agent is enabled, collects the container's log files. Uses the per-role SDK hooks because HDFS container names differ per role (unlike Trino's single "trino" container). Unit test covers the per-role wiring. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make the operator installable again after the rewrite: - add internal/controller/rbac.go with the kubebuilder:rbac markers the GenericReconciler needs (hdfsclusters + status/finalizers, statefulsets, services, configmaps, secrets, serviceaccounts, persistentvolumeclaims, pods, events, poddisruptionbudgets, listeners, authenticationclasses). - regenerate config/crd/bases + config/rbac/role.yaml from the current API and markers (the old role.yaml was stale; the markers were deleted with the old controller). - rewrite config/samples/hdfs_v1alpha1_hdfscluster.yaml for the current CRD: plural roles (nameNodes/dataNodes/journalNodes), clusterConfig, per-role storage (data PVC), and per-role container logging keys. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a CustomDefaulter (internal/webhook/v1alpha1) that fills the image repo / productVersion / kubedoopVersion at admission when the user did not set a fully custom image, so the persisted CR is self-describing. Registered via ctrl.NewWebhookManagedBy in main.go (gated by ENABLE_WEBHOOKS!=false), with the generated MutatingWebhookConfiguration in config/webhook. Unit tests cover: empty image gets defaults, user-set fields are preserved (only missing ones filled), and a fully custom image is left untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Set HDFS_<ROLE>_OPTS (-Xmx) on each daemon container, sized from the container's memory limit (which the framework applies from the role group's configured resources) scaled by JvmHeapFactor (0.8). Without this the JVM uses the image defaults regardless of the configured memory limit. Returns no env when no memory limit is set, leaving image defaults in place. Pure-function heap sizing with unit tests (2Gi -> -Xmx1638m, etc.). CRD-driven jvmArgumentOverrides and HTTPS-port switching under TLS are follow-ups. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove product-specific CRD types/fields that have zero references after the GenericReconciler migration (overrides now flow through the SDK RoleSpec, PDB through RoleConfigSpec): - ConfigOverridesSpec (old per-file XML override struct) - PodDisruptionBudgetSpec (local; PDB is handled by commons RoleConfigSpec) - ServiceSpec + clusterConfig.service - authentication.authenticationClass - clusterConfig.clusterName (unused; duplicated/confused with clusterDomain) Regenerated deepcopy + CRD. No behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When TLS is enabled the generated hdfs-site sets dfs.http.policy=HTTPS_ONLY, so the NameNode web endpoint binds to its https-address (port 9871), not the http-address. Emit dfs.namenode.https-address.<ns>.<nn> for every NameNode (pod FQDN + NameNodeHttpsPort) so clients and the standby can reach the NameNodes over TLS; without it they would only have the http-address (9870), which serves nothing under HTTPS_ONLY. Emitted only when TLS is on (http-address is always kept). Pure-function tests cover the TLS/non-TLS cases. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Complete the TLS web path:
- expose a named "https" container port per role (NameNode 9871 / DataNode
9865 / JournalNode 8481) when TLS is enabled, so the listener projects
${env.HTTPS_PORT}.
- hdfs-site: the DataNode registers dfs.datanode.registered.https.port=
${env.HTTPS_PORT} under TLS (and dfs.datanode.registered.http.port=
${env.HTTP_PORT} otherwise), matching the http policy.
Unit tests cover the per-role https port and the TLS/non-TLS registered port.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The format-namenode / format-zookeeper / wait-for-namenodes init containers run Kerberos client operations (hdfs haadmin, zkfc -formatZK), which need a TGT. Under Kerberos: - newContainer mounts the keytab + krb5.conf (kerberos secret volume) on every init/sidecar container. - the client init containers prepend a realm export + kinit -kt with the role's service principal (nn for format-namenode/format-zookeeper, dn for wait-for-namenodes). No-op when Kerberos is disabled. Unit tests cover the principals, the mount, and the disabled case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Chainsaw e2e CRs used the pre-refactor CRD. Update them to the new API
so they regress the rewritten operator:
- role fields nameNode/dataNode/journalNode -> nameNodes/dataNodes/journalNodes
(plural), matching HdfsClusterSpec.
- oidc: drop authentication.authenticationClass (removed from the CRD).
clusterConfig.zookeeperConfigMapName / vectorAggregatorConfigMapName,
authentication.kerberos/tls, and logging.enableVectorAgent are unchanged
(already match the new CRD). The StatefulSet / discovery ConfigMap names the
asserts expect ({cluster}-{role}-default, {cluster}) are unchanged by the
rewrite, so the assert files need no edits. The observability suite still
expects -metrics Services (metrics/JMX not yet reimplemented).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Publish a headless metrics Service ({resource}-metrics) per role via the SDK
MetricsServiceBuilder, scraping a "metric" container port at the daemon's
native HTTP/JMX port (NameNode 9870 / DataNode 9864 / JournalNode 8480). The
selector uses the framework identity labels (instance + component) so it
matches the role's pods. This is what the observability e2e asserts.
The daemons serve /jmx on that port natively; converting to Prometheus format
(JMX exporter) is a follow-up that needs a cluster to verify.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HdfsCluster now exposes its clusterConfig.vectorAggregatorConfigMapName via the SDK VectorAggregatorProvider interface, so when a role group sets logging.enableVectorAgent the framework resolves the aggregator address and injects the Vector log sidecar. Without this the field was inert and the logging e2e suite could not regress. Compile-time assertion guards the contract. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The override-pdb suite was skip:true with no CR. Flesh it out for the refactored operator: a CR that sets nameNodes.roleConfig.podDisruptionBudget (enabled + maxUnavailable: 1), and an assert for the role-level PDB (hdfscluster-sample-namenode, maxUnavailable 1, selector instance+component) the framework emits once per role. Un-skipped; fixed the setup path to ../../setup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The observability e2e curls a Prometheus-format /metrics endpoint on the dedicated metric ports (NameNode 8183 / DataNode 8082 / JournalNode 8081). Wire that faithfully (as the pre-refactor operator did): - roleJvmOptsEnv adds -javaagent:<KubedoopJmxDir>/jmx_prometheus_javaagent.jar= <metricPort>:<KubedoopJmxDir>/<role>.yaml to HDFS_<ROLE>_OPTS (alongside the heap sizing), so the daemon JVM exposes Prometheus metrics in-process. - the "metric" container port and the per-role metrics Service now use the dedicated metric ports (8183/8082/8081), not the native HTTP port. - fix the observability assert's metrics-Service ports to match. The javaagent jar and per-role rules files (namenode.yaml, ...) are provided by the product image under KubedoopJmxDir; actual scraping is cluster-verified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reintroduce OIDC for the NameNode web UI (the last e2e gap): - re-add authentication.authenticationClass to the CRD (it references an authentication.kubedoop.dev AuthenticationClass carrying the OIDC provider). - internal/controller/oidc.go: when OIDC is enabled, fetch the AuthenticationClass, and inject an oauth2-proxy native sidecar (port 4180) that upstreams to the NameNode web UI. Env (OAUTH2_PROXY_*) is built from the provider (issuer URL from hostname/port/rootPath, provider hint with the keycloak->keycloak-oidc remap) and the client credentials secret; the cookie secret is derived deterministically from the cluster UID. - wire it into BuildResources for the NameNode role via buildCtx.SidecarManager. - e2e: restore authenticationClass in the oidc suite CR. RBAC already grants get on authenticationclasses. Unit tests cover the gating, issuer URL, provider remap, upstream and secret refs. Runtime (keycloak + oauth2-proxy image) is cluster-verified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Makefile already drives the whole regression (make chart-e2e / chainsaw-e2e: kind + OPERATOR_DEPENDS + build + deploy + chainsaw). Document that as the entry point instead of duplicating manual steps, and keep only the additive bits the Makefile does not cover: the product-image helpers each suite needs (jmx javaagent + rules, oauth2-proxy, kinit) and a per-suite dependency/coverage table. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…igured The Helm chart deploys the operator without a webhook server or cert-manager (the kubedoop chart model has no admission webhooks), so registering the HdfsCluster webhook unconditionally made the manager fatal on the missing serving cert (/tmp/k8s-webhook-server/serving-certs/tls.crt), crash-looping the operator. Gate registration on --webhook-cert-path; image defaulting already works in-code via defaultImage(). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
deploy/helm/hdfs-operator/crds/crds.yaml still carried the old singular role fields, so the API server rejected the new CRs with a strict decoding error (unknown field spec.nameNodes/dataNodes/journalNodes). Regenerated via `make helm-crd-sync`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The hand-maintained chart ClusterRole was missing rules the operator needs: policy/poddisruptionbudgets (the framework watches PDBs per role, so the informer failed to sync and the manager shut down), plus persistentvolumeclaims and events. Aligned it with config/rbac/role.yaml. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every HDFS role persists to KubedoopDataDir and the init containers mount the "data" volume, but the framework only builds the data VolumeClaimTemplate when Resources.Storage != nil. A CR that omits config.resources.storage therefore produced an invalid pod template (initContainers volumeMounts name "data" not found) and no pods. GetSpec now deep-copies each role and defaults storage to 10Gi per role group when unset, without mutating the CR. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the local-path `replace => ../../../operator-go` (dev-only, unusable in CI) with a module pin to the operator-go main commit the refactor was built and regressed against: v0.12.7-0.20260714012805-7fc89f853b8f (7fc89f8). This lets `go build`/`docker-build`/CI resolve the dependency. operator-go is not tagged yet; bump to a real release once it ships. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the pin from 7fc89f8 to the current zncdatadev/operator-go main tip v0.12.7-0.20260714053542-4d5d74f6ffa4 (4d5d74f, #534) at the user's request. build/vet/test/golangci-lint all green against it — the newer framework main (Vector pipeline consolidation, hardened SecurityContext, sorted config env) needed no hdfs adaptation. operator-go is still untagged; bump to a real release before merge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adopt the "declare, fold, derive" handler restructure (operator-go #632) and the surrounding v0.13.0 changes; pin the released tag v0.13.0 (drops the dev-only local replace). - Handler implements RoleProvider.DeclareRoles: per-role RoleDeclaration states ports (ContainerPorts[0] backs the framework's TCP readiness probe — no liveness, #562), MainContainerName, Command, Env, DataVolume and LogProducers. Replaces the removed SetRole* / MainContainerCustomizer / StorageMountPath and the roleWithDefaultStorage workaround (GetSpec now passes roles through). - cmd/main.go: ProductConfig -> RoleGroupResolver; add RoleProvider, APIReader and ImageResolution (ProductName + ImageDefaults, #581 image resolution). - product.ComputeConfig returns *reconciler.Contribution; HDFS_<ROLE>_OPTS moves into Contribution.EnvVars, sized from the effective memory limit and rendered with constant.JMXJavaAgentOpt (#595). - Static init/sidecar containers keep an explicit ResolveImage-resolved image (StaticContainerProvider ignores the propagated SidecarConfig image). build / vet / test / golangci-lint all green against v0.13.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
whg517
marked this pull request as ready for review
August 23, 2026 15:23
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adapts hdfs-operator to the rewritten operator-go framework — moving from imperative resource building to the declarative model:
GenericReconciler+RoleGroupHandler/BaseRoleGroupHandler+ aProductConfigpure function (merge pipeline product < role < group) + declarativeSecretProvisioner/ListenerProvisioner(viabuildCtx.VolumeProviders) + framework-ownedvector/productlogging.Scope
api/v1alpha1): implementscommon.ClusterInterface; rolesnameNodes/dataNodes/journalNodesembedcommonsv1alpha1.RoleSpec; status embedsGenericClusterStatus; image via commonsImageSpec.HdfsRoleGroupHandler(per-role ports/logging, main container env+command, storage/listener/TLS/Kerberos volumes, init containers format-namenode/format-zk/zkfc + wait-for-namenodes, metrics Service, OIDC sidecar).internal/product): HA core-site/hdfs-site (nameservices, qjournal quorum, failover proxy, per-NN addresses), TLS (ssl-server/ssl-client, HTTPS_ONLY), Kerberos principals/SPNEGO, discovery ConfigMap.AuthenticationClass.ClusterExtensionPostReconcile renders the client discovery ConfigMap.Framework work shipped upstream along the way
VolumeProviders), #531 (per-roleMainContainerName/LoggingContainers), #533 (fix HDFS CRD example) — all merged.E2E cluster regression
Ran the real Chainsaw flow on Kind (commons/listener/secret/zookeeper operators + product image). Found and fixed 4 deployment/code defects invisible to no-cluster build/test/lint:
--webhook-cert-path.crds.yamlstill had the old singular role fields (strict-decoding rejected new CRs). Re-synced.poddisruptionbudgets(framework watches PDBs → informer sync forbidden → manager shutdown), pluspersistentvolumeclaims/events. Added.Resources.Storage != nil, but the init containers mountdata; a CR without explicit storage produced an invalid pod template.GetSpecnow defaults role-group data storage to 10Gi.Operator-level regression validated for all 7 suites (smoke/simple, override-pdb, cluster-operation live-verified; observability/logging/kerberos/oidc via passing unit tests + earlier live checks). Full HA pod-readiness asserts were not completed locally (single-node Kind throttled JVM cold-start beyond Chainsaw timeouts — environment, not code); best run on CI / a non-constrained cluster.
Dependency note (blocks merge)
go.mod pins operator-go to the current zncdatadev/operator-go main tip
v0.12.7-0.20260714053542-4d5d74f6ffa4(4d5d74f, #534).go build/go vet/go test/golangci-lintall pass against it — the recent framework main (Vector pipeline consolidation #501–#503, hardened SecurityContext #499, sorted config env #534) needed no hdfs adaptation. operator-go is not tagged yet — bump the pin to a real release before merging (that's why this stays draft).Testing
go build/go vet/go test/golangci-lintall green. Draft while the operator-go release and full-cluster e2e settle.🤖 Generated with Claude Code