operator: Connect Pipeline CRD + controller (revives #1337 with enterprise-branch enhancements)#1677
Conversation
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
E2E evidence: mysql_cdc from RDS + Iceberg/Glue fanout on EKS (IRSA, zero static credentials)Ran this branch ( What the flow exercised
Results
Operational notes potentially worth folding into docs
Nice work — |
4-Reviewer Panel ReviewIndependent review panel over the full PR range (
Blocker
Major
Minor
Nit
One positive note from the adversarial pass: Codex specifically checked the finalizer path for a PodMonitor deletion deadlock and found none — the common syncer skips unknown API types safely. Highest-leverage fixes before merge: the README rewrite (#1), the TLS resolution pair (#2, #3), the ignored clusterRef namespace (#4), and the SSA hijack guard (#8). 🤖 Generated with Claude Code — 4-reviewer panel (staff engineer, documentation, security, Codex adversarial) |
Adds a `Pipeline` CRD (`cluster.redpanda.com/v1alpha2`) that manages Redpanda Connect pipelines as first-class Kubernetes resources, statically typed against the same v1alpha2 primitives the other CRDs use: - `cluster` (*ClusterSource): point at a Redpanda CR (clusterRef) or supply brokers/TLS/SASL inline (staticConfiguration). The operator inline-merges seed_brokers/tls/sasl into the user's input.redpanda / output.redpanda blocks. The non-deprecated `redpanda` plugin family is the merge target; legacy `redpanda_common` is not auto-configured. - `userRef`: optional binding to a User CR (Secret-backed SCRAM password + mechanism, username from User.metadata.name). The User CR stays user-managed so ACL scoping is auditable. - `serviceAccountName`: SA bound to the pipeline pod, for per-pipeline cloud-IAM trust (IRSA/Workload Identity/Pod Identity). - `valueSources`: typed named env-var projections (inline / configMapKeyRef / secretKeyRef / externalSecretRef). - `image`: per-pipeline Connect runtime override with three-tier precedence (spec.image > chart connectController default via --connect-default-image > binary-baked PipelineDefaultImage). - `configYaml`: the user's Connect pipeline YAML, inline catch-all. CEL on PipelineSpec enforces the contract: userRef is forbidden alongside cluster.staticConfiguration, and forbidden without cluster.clusterRef. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On top of the revived Pipeline CRD (#1337), port the improvements the feature accumulated in the enterprise repo, adapted to this repo's conventions: - Render the resolved cluster connection as the top-level redpanda: shared client (redpanda_common support) in addition to merging into input.redpanda/output.redpanda; user-set keys still win. - Stamp pipeline pod templates with a sha256 config checksum so configYaml/configFiles changes roll the Deployment. - spec.extraInitContainers: ordered init-container passthrough ahead of the built-in lint container. - spec.affinity for node-pool scheduling. - Never tear down running pipelines on reconcile failures (clusterRef/ userRef/license): surface on status, keep last-known-good children. - Memoize the clusterRef chart render per cluster generation and bound reconcile concurrency (MaxConcurrentReconciles). - staticConfiguration wiring with CA path + SASL fallback fixes. - Connect telemetry: pipeline fleet stats (counts, replicas, images, node spread) collected by the central telemetry runnable, with a connectController feature flag from cmd/run. - Bump the default Connect image to 4.100.0. Telemetry is wired through cmd/run's existing reporter rather than the enterprise branch's controller-registered runnable (that shape existed only because the enterprise repo has no cmd/run yet). Tests asserting teardown-on-failure and no-shared-client were updated to the new contracts; chart schema/partials/goldens regenerated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
golangci-lint --fix + generators left a residual diff in CI: gci import grouping in the deprecations test, the enterprise-port ossv1alpha2 alias collapsed into the existing redpandav1alpha2 import in the pipeline controller, chart README + generated status regen, and test import ordering. Applied verbatim from the CI job's git diff output. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lizer behavior Pipeline CRs carry an operator finalizer, so toggling connectController.enabled off while Pipeline CRs exist blocks their deletion until the controller returns (already noted in the controller source). Surface that in the values docs, prompted by e2e testing and by the equivalent review finding on the ShadowLink flag (#1675). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes for the 4-reviewer panel review on the Pipeline CRD PR — the
blocker, all majors, and the minors/nits.
TLS / connection resolution:
- clusterRef TLS now maps every shape the chart legitimately renders:
Secret-backed CAs, ConfigMap truststores, and requireClientAuth
client cert/key pairs (projected at /etc/tls/certs/client and
presented via client_certs). Previously only Secret CAs survived, so
truststore/mTLS listeners were dialed without TLS material.
- staticConfiguration honors the CommonTLS contract: tls: {enabled:
false} with no certificate material now disables TLS, and cert/key
(mTLS) are honored — private keys must be Secret-backed.
- clusterRef.namespace/group/kind are rejected (CEL at admission plus a
controller guard) instead of being silently ignored and binding the
pipeline — with superuser credentials — to a same-named cluster in
its own namespace.
Controller correctness / hardening:
- clusterConnCache entries are keyed by CR UID + generation and evicted
on cluster deletion, so a delete/recreate never serves stale
brokers/TLS/SASL and the cache cannot grow unboundedly.
- The SSA sync refuses to adopt same-named objects the Pipeline does
not own (Ready=False/NameConflict) instead of force-applying over
them and deleting them with the Pipeline.
- The PodMonitor CRD probe consults the manager's RESTMapper; the
previous cached List always failed with ErrCacheNotStarted before
mgr.Start() (skipping the watch on every real deployment) and
hardcoded the "default" namespace.
- userRef resolution validates the User's password Secret and key;
valueSources entries are resolved at reconcile time and reported via
the ValueSourcesResolved condition (previously documented but never
set). Failures keep last-known-good children running.
- Referenced Secret/ConfigMap rotations roll pipeline pods via a
credentials-checksum pod-template annotation built from
resourceVersions (plus the license bytes) — never secret contents.
- Inline staticConfiguration SASL passwords are mirrored into a
Pipeline-owned <name>-sasl Secret instead of rendering as plaintext
EnvVar values on the pod spec.
- Transient clusterRef/userRef/valueSources/license failures derive
phase and Ready from the live Deployment and overlay only the failing
condition (new License condition type): a Running pipeline no longer
flips to Pending while its workload keeps processing data.
- replicas: 0 now reports phase Stopped, matching the documentation;
lint condition messages are truncated at 1KiB to bound log-content
exposure via pipelines/get.
- New spec.extraVolumes / spec.extraVolumeMounts make the documented
extraInitContainers staging pattern produce a valid Deployment.
Wiring / telemetry / chart:
- --enable-connect without --enable-redpanda-controllers is rejected at
startup like NodePool/Console; --license-file-path help now documents
that it gates the Connect controller.
- The operator chart CSV-quotes --common-annotations and
--connect-monitoring-labels pairs, so a comma in a value no longer
fails pflag parsing and crash-loops the operator.
- Telemetry resolves the effective Connect image through the
--connect-default-image tier and reports only tags/short digests,
never repositories; itemized pipeline RBAC gains users get/list/watch.
Docs / meta:
- The pipeline README is rewritten around the real enablement path
(connectController.enabled + enterprise.licenseSecretRef, or
--enable-connect + --license-file-path) — no more personal-repo
runner, REDPANDA_LICENSE_FILE, or --enable-connect-controller; stale
connect:4.96.0 references bumped to 4.100.0; license-Secret
mirroring, credential-rotation rolls, and the full condition set are
documented.
- values.yaml: license phrasing corrected (operator-level, not
per-CR), the orphaned additional-controllers comment removed, and the
commented image example bumped to 4.100.0.
- The malformed duplicate changie entry is deleted and the remaining
feature entry updated; kubectl-facing type docs now say Pipeline
(not Connect); the bootstrap-superuser fallback of
clusterRef-without-userRef carries an explicit SECURITY note.
Tests: successful clusterRef resolution against a real Redpanda CR
(TLS material + UID-keyed cache invalidation on recreate), the
clusterRef scope guard, userRef password-Secret validation,
valueSources validation, the ownership-conflict guard,
credentials-checksum rotation, static TLS semantics, mTLS rendering,
extra volumes/mounts, inline-SASL mirroring, SetupWithManager with the
PodMonitor probe (pre-start), telemetry image anonymization, and a
pflag round-trip for the annotation quoting. Acceptance pipeline steps
now gate on observedGeneration and a fully-rolled Deployment so the
update scenario cannot pass vacuously against stale status.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
672999d to
b861c26
Compare
|
Docs-side validation report (building this branch at head CRD install failure (blocker)On kind v1.28.0, both The four Suggestion: an acceptance/CI step that applies the generated CRDs against a real kube-apiserver (kind or envtest with validation enabled) would catch this class of issue — envtest may not enforce the CEL cost budget the way a real API server does. What I validated (with the CRD locally patched to remove the over-budget rules)Everything below worked as designed at head
Two behaviors worth a deliberate decision (I'll document them either way):
Context: docs draft for this feature is redpanda-data/rp-connect-docs#457 (DOC-2275). |
|
Thanks @JakeSCahill will take a look. |
Validation follow-up: CRD install is version-dependent; supported range is k8s 1.33–1.36Thanks @JakeSCahill — your finding reproduces exactly, and I dug into the version boundary. Summary: this operator release supports k8s API server 1.33–1.36, and the Pipeline CRD installs cleanly across that entire range. The rejection you hit is confined to k8s ≤1.29, which is below the supported range. CRD install vs. k8s versionBracketed with a direct server-side apply of the generated CRD (head
Root cause is the k8s CEL cost estimator, which became more accurate in newer apiextensions-apiservers — the same schema is over budget on ≤1.29 and under budget on ≥1.33. The EKS 1.36 end-to-end (the support ceiling)On EKS
(One toolchain note so it doesn't trip others: the operator chart's CRD hook stalls under helm v4.2.1 — the pre-install-hook |
|
Correction to my earlier report: the CRD install blocker does not reproduce on a supported Kubernetes version — it was an artifact of my test environment running K8s 1.28, below the 26.2 support range. Retested today at head
So the only real finding there is informational: the CRD cannot install on much older API servers (1.28) because their CEL cost estimator rejects the Everything in the functional-validation section of my earlier comment stands (that testing exercised the controller, not the API server): lint, lifecycle, valueSources, config rollout, clusterRef + TLS injection, and the two documented behaviors (missing-topic pipelines report Docs side: our stretch guide stated "Kubernetes 1.28 or later" as a prerequisite, which is what put my test rig on 1.28 in the first place — fixing that now to point at the supported-versions compatibility matrix instead. |
…caling Wire the Pipeline CRD's /scale subresource to .spec.replicas, .status.replicas, and a new .status.selector (stamped on every status write with the fixed pipeline pod selector), so kubectl scale, HorizontalPodAutoscaler, and KEDA ScaledObjects can drive pipeline replicas without fighting the operator's Deployment sync. CPU/memory HPAs work as-is since pipeline pods always carry resource requests; Connect-emitted metrics (input_received, output_sent, ...) scale via prometheus-adapter custom metrics or KEDA prometheus/kafka triggers. spec.paused keeps precedence over autoscaler-written counts, and an explicit replicas: 0 (KEDA scale-to-zero) is preserved rather than re-defaulted to 1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
| Scenario | Result |
|---|---|
kubectl scale + raw GET /scale |
proper Scale object w/ selector; Deployment follows in seconds |
| HPA cpu+memory | cpu: 300%/50% → scaled 1→4 in ~30s |
KEDA kafka-lag: 12-partition topic → consumer-group pipeline → RDS Postgres sql_insert |
lag ~500k → 1→6, partitions rebalanced across all 6 pods; 596,075 rows, 7 distinct writer pods, 0 duplicate ids; after load stop, drained to lag=0 and stepped down 6→4→1 |
KEDA prometheus on input_received (via operator PodMonitor) |
scaled 1→3 in ~90s |
Pipelines stayed Ready=True through every rebalance and scale transition. Full details in the README's new "Autoscaling with HPA or KEDA" section.
🤖 Generated with Claude Code
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The scale-subresource/autoscaling capability folds into the main Pipeline entry rather than a separate call-out. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
Adds a
PipelineCRD (cluster.redpanda.com/v1alpha2) that manages Redpanda Connect pipelines as first-class Kubernetes resources. The spec is statically typed against the same Kubernetes-native primitives the rest of the v1alpha2 CRDs already use:cluster(*ClusterSource) — same primitive Topic/User use. Either point at a Redpanda CR (clusterRef) or supply brokers/TLS/SASL inline (staticConfiguration). When set, the operator inline-mergesseed_brokers,tls, andsaslinto anyinput.redpanda/output.redpandablocks the user wrote inconfigYaml, and additionally renders the resolved connection as the top-levelredpanda:shared client soredpanda_common(and any shared-client plugin) works without inline credentials. User-set keys win on conflict.userRef— optional alongsidecluster.clusterRef. Binds the pipeline to aUserCR; the operator reads the User's Secret-backed password + SCRAM mechanism and usesUser.metadata.nameas the SASL username. The User CR stays user-managed so ACL scoping is auditable (operator does not auto-create or modify it). Omit for unauthenticated clusters.Why
userRefis flat whilecluster.clusterRefis wrapped.clusteris aClusterSource— a discriminated union of two sources: point at a Redpanda CR (clusterRef: { name }) OR supply brokers/TLS/SASL inline (staticConfiguration: {...}). The wrapping exists to express that union, and it matches the existingTopic/UserCRDs (spec.cluster: *ClusterSource).userRefhas only one source today — point at aUserCR by name — so it stays flat:userRef: { name }. The inline-SASL counterpart already lives atcluster.staticConfiguration.kafka.sasl.{mechanism,username,password}(also flat — also a single source), and CEL forbids combining it withuserRef. If a future use case needs a second user-identity source (e.g., inline SASL without aUserCR backing it), we can promoteuserRefinto auser: UserSourcewrapper at that point.serviceAccountName— theServiceAccountbound to the pipeline pod. When unset, the namespace's default SA is used. Set this to scope cloud-IAM trust (IRSA on EKS, Workload Identity on GKE, Pod Identity on AKS) per-pipeline rather than sharing the namespace's default SA across every pipeline. The operator does not create the SA — provision it (with the cloud-IAM annotations) out-of-band.valueSources— typed list of named env-var projections (one named pull per entry) backed by inline /configMapKeyRef/secretKeyRef/externalSecretRef. Replaces the earliersecretRef[]env-splat andenv[]rawcorev1.EnvVarapproaches.image— per-pipeline Connect runtime override. Three-tier precedence:Pipeline.spec.imagewins, then the chart-levelconnectController.image.{repository,tag}default (plumbed in via the operator's--connect-default-imageflag), then the binary-bakedPipelineDefaultImageconstant.configYaml— the user's Connect pipeline YAML. Stays the inline catch-all for anything the typed fields don't cover.CEL on
PipelineSpecenforces the contract:userRefis forbidden alongsidecluster.staticConfiguration(the static path carries its own inline SASL), anduserRefis forbidden withoutcluster.clusterRef(no cluster context to authenticate against otherwise).userRefis otherwise an opt-in for SASL-enabled clusters.Worked examples
A. Cluster-bound — Pipeline points at a Redpanda CR on the same Kubernetes cluster
The user provisions a SCRAM
UserCR with ACLs scoped to what the pipeline reads/writes; thePipelinethen references both the cluster and the user.User CR (separate manifest, owns the SCRAM identity + ACLs):
Pipeline CR:
What the operator renders into the pod's
/config/connect.yaml:Pod env (auto-derived):
REDPANDA_SASL_USERNAME="orders-to-warehouse"(literal, fromUser.metadata.name)REDPANDA_SASL_MECHANISM="SCRAM-SHA-512"(literal, fromUser.spec.authentication.type)REDPANDA_SASL_PASSWORD←secretKeyRef: { orders-to-warehouse-password, password }(fromUser.spec.authentication.password.valueFrom.secretKeyRef)S3_SECRET_KEY←secretKeyRef: { s3-creds, secret_access_key }(fromvalueSources)User keys win on conflict. If the user had written
seed_brokers: [external.example.com:9093]insideinput.redpanda, the operator would have left that value untouched and only injected the missingtlsandsaslkeys. That's the escape hatch for cluster-bound pipelines that need to point a specific input/output at a different cluster.B. External Kafka / BYOC — static configuration
For pipelines reaching an external Redpanda or Kafka the operator doesn't run (Redpanda Cloud BYOC, cross-region tap, Confluent Cloud, MSK, etc.). No
userRef; SASL credentials live inline on the static config and the password is itself aValueSource.Pipeline CR:
What the operator renders:
staticConfigurationandclusterRefproduce the same inline-merge contract — only the source-of-truth for the connection fields differs. NoUserCR involved.C. Per-pipeline IRSA — native RDS IAM database authentication via
serviceAccountNamePipeline binds to a Redpanda cluster for its output and to RDS for its CDC input. The pipeline pod itself calls AWS APIs (
rds:GenerateDBAuthToken) using an IAM role assumed via IRSA.serviceAccountNamescopes that trust to this one Pipeline — no other workload in theredpandanamespace can assume the role.Out-of-band: ServiceAccount with the IRSA annotation (terraform / pulumi / a separate manifest — the operator does not create it):
Pipeline CR:
The pipeline pod assumes
mysql-cdc-orders-rds(and only that role) via the projected service-account token at/var/run/secrets/eks.amazonaws.com/serviceaccount/token. The MySQL connection uses an IAM-generated token; no MySQL password lives anywhere in the pod, the Pipeline CR, or a Secret.This is the production K8s-RDS pattern: IRSA gates AWS API access (so the pipeline can mint MySQL tokens); Pipeline
userRefgates Redpanda access (so the pipeline can write its output topic). The two trust boundaries are orthogonal.D. Inline — pipeline references multiple external sources via
valueSourcesFor pipelines whose primary connection isn't Redpanda at all, or that fan out to multiple non-Kafka backends.
valueSourcesis destination-agnostic: each entry is a typed pull from inline / Secret / ConfigMap / ExternalSecret and projects to an env var the YAML references via${NAME}. Connect plugins read those env vars however they expose credentials.Properties this design intentionally preserves:
input.redpandaandoutput.redpandablocks. New Connect plugins ship in future Connect releases without any operator change; non-redpandablocks (mongodb,snowflake_put,sql_insert,aws_s3,mysql_cdc, etc.) pass through untouched.inline/secretKeyRef/configMapKeyRef/externalSecretRefcan mix freely across entries in the samevalueSourceslist.secretRef[]env-splat, every value is a named pull. Unused keys in a Secret don't leak into the pod env, the env name and the Secret's key can differ, and multiple pipelines can pull non-overlapping keys from the same Secret.cluster+userRefgates Redpanda access;serviceAccountNamegates cloud-IAM access. Either can be set without the other.Status conditions
ClusterRefcluster.clusterRefresolved → broker list + TLS material loadedClusterRefInvalid(cluster not found / not Ready)UserRefuserRef.nameexists, haspassword.valueFrom.secretKeyRefset, mechanism resolvedUserInvalid(User CR not found, missing Secret-backed password)ConfigValidredpanda-connect lintpassesConfigInvalidReadyTests
TestRender_InlineMergesRedpandaPlugins— six subtests covering the cluster-binding render path: merges intooutput.redpanda, merges intoinput.redpanda, user-supplied keys win on conflict, no*.redpandablock in user config → no injection,output.redpanda_commonis intentionally not auto-configured (regression guard against re-emitting a top-levelredpanda:block), fully-inline pipeline (no cluster binding) passes through unchanged.TestRender_Deployment_ServiceAccountName— propagation ofspec.serviceAccountNametoDeployment.Spec.Template.Spec.ServiceAccountName; empty when unset.TestRender_Deployment_ImagePrecedence— three subtests, one per image precedence tier (per-pipelinespec.image> chart-levelconnectController.image> binary constant).TestRender_Deployment_ValueSources— assertsEnvFromis empty on both the lint init and the connect container, and that eachValueSourceentry projects as exactly one typedEnvVar(inline →Value;secretKeyRef/configMapKeyRef→ValueFrom.{Secret,ConfigMap}KeyRef).TestReconcile_InvalidClusterRefCleansUpManagedResources—cluster.clusterRefresolution failure short-circuits before user resolution; status surfacesClusterRefInvalidand managed resources are torn down.TestRender_*cases cover Replicas, Paused, Zones, Resources, Annotations, Topology, Budget, ConfigFiles, MonitoringPodMonitor.task lint(helm lint + golangci-lint + actionlint) clean.task generateclean (no further diff).End-to-end validation
The branch was exercised against real AWS infrastructure (EKS 1.34 + RDS MySQL 8.0 with
iam_database_authentication_enabled=true) using the Example C scenario (per-pipeline IRSA + native RDS IAM auth + Pipeline CR writing to Redpanda). MySQL snapshot rows reached themysql.shop.orderstopic with no MySQL password anywhere in the Pipeline CR, the pod env, or any Secret. See the e2e comment thread for the full run + reproduction steps.Enhancements on top of #1337 (ported from the enterprise branch)
redpanda:shared client rendered from the resolved cluster connection (in addition to theinput.redpanda/output.redpandamerge) — makesredpanda_commonand other shared-client plugins work without inline credentials; user-set keys still win. (This supersedes Add Pipeline CRD for Redpanda Connect pipeline management #1337's deliberate non-configuration ofredpanda_common; the description above has been updated accordingly.)cluster.redpanda.com/config-checksum) —configYaml/configFileschanges roll the Deployment. Connect reads its config once at startup from the mounted ConfigMap, so without this a config change never reached running pods.spec.extraInitContainers— ordered init-container passthrough that runs ahead of the operator's built-inlintinit container, so anything staged into a shared volume (certs, warmed caches, dependency waits) is visible to lint and the runtime.spec.affinity— pod affinity/anti-affinity for node-pool scheduling of pipeline pods..status(with requeue) without tearing down running pipelines; the last-known-good children keep processing data. Only deleting the Pipeline CR removes them. Previously a transient API hiccup or a briefly-unreadable license Secret would delete the pipeline Deployment mid-stream.MaxConcurrentReconcilesbounds parallel reconciles for large pipeline fleets.staticConfigurationfixes — CA path and SASL fallback corrections in the rendered config.connectControllerfeature flag reported fromcmd/run.4.100.0.Behavior-contract test updates
TestReconcile_Invalid{License,ClusterRef}CleansUpManagedResources→…KeepsManagedResources(children survive failed reconciles).TestRender_InlineMergesRedpandaPluginsnow asserts the top-level shared client is rendered (previously asserted its absence).Testing
operator/internal/controller/pipeline(envtest + goldens),operator/internal/telemetry,operator/chart(template goldens), andoperator/cmd/...all pass;golangci-lintclean. The underlying features were validated end-to-end on EKS + RDS MySQL (mysql_cdcsnapshot + live binlog via aredpanda_commonoutput, config-change auto-roll with no manual pod delete).🤖 Generated with Claude Code