Skip to content

feat(operator): add migration orchestration controller - #346

Closed
Siddhant-K-code wants to merge 32 commits into
siddhant-k-code-authored-migration-hooksfrom
siddhant-k-code-authored-migration-controller
Closed

Siddhant-K-code wants to merge 32 commits into
siddhant-k-code-authored-migration-hooksfrom
siddhant-k-code-authored-migration-controller

Conversation

@Siddhant-K-code

@Siddhant-K-code Siddhant-K-code commented Aug 24, 2026

Copy link
Copy Markdown
Member

Summary

Adds the Go controller that coordinates OpenFGA datastore migrations before application rollout.

  • Derives the desired OpenFGA version and migration configuration from the target Deployment.
  • Creates and observes migration Jobs, then records completed versions in a status ConfigMap.
  • Handles retries, stale Jobs, failure conditions, and upgrade sequencing.
  • Rejects unmanaged Job and ConfigMap name collisions instead of overwriting unrelated resources.
  • Uses ownership checks plus UID and resource-version preconditions for safe deletion.
  • Includes focused controller coverage for success, failure, collision, and race scenarios.

Stack

Native stack 351. Open #345 to view the GitHub stack map. Review bottom to top:

  1. fix(chart): harden external migration hooks #345: External migration hooks, base main
  2. feat(operator): add migration orchestration controller #346: Migration controller (this PR), base fix(chart): harden external migration hooks #345
  3. feat(operator-chart): package the migration operator #347: Operator Helm chart, base feat(operator): add migration orchestration controller #346
  4. feat(chart): integrate operator-managed migrations #348: OpenFGA chart integration, base feat(operator-chart): package the migration operator #347
  5. ci(operator): add build and upgrade coverage #349: Operator CI and upgrade E2E, base feat(chart): integrate operator-managed migrations #348
  6. docs(operator): document migration architecture decisions #350: Migration architecture ADRs, base ci(operator): add build and upgrade coverage #349

Replaces closed #337 and carries the controller portion of source #331. Helm packaging and chart integration remain in later layers.

Attribution

The 24 original controller commits are replayed with Ed Milic (@emilic) as their actual Git author, preserving their original author dates, messages, and commit boundaries. GitHub links the replayed commits to @emilic.

Validation

emilic and others added 25 commits August 24, 2026 18:57
Replace Helm hook-based migrations with a lightweight Kubernetes
operator that watches OpenFGA Deployments, detects version changes,
and runs migrations as regular Jobs.

- Go operator using controller-runtime (no CRDs)
- Helm subchart with opt-in via operator.enabled (default false)
- Dedicated migration ServiceAccount (separate from runtime)
- Auto-recovery on database failure (delete/retry cycle)
- GitHub Actions workflow for multi-arch image builds
- Integration test values for local Kubernetes clusters

Resolves #211, #107, #120, #100, #126
- Harden pod security (runAsNonRoot, seccompProfile, drop ALL caps)
- Find container by name instead of index to handle sidecars
- Skip migration for memory datastore
- Persist retry-after annotation before Job deletion to survive re-enqueue
- Clear MigrationFailed condition on success
- Propagate imagePullSecrets and securityContext to migration Jobs
- Remove unused RBAC rules (secrets, serviceaccounts)
- Add POD_NAMESPACE downward API for namespace-scoped watch default
- Remove no-op migration values (timeout, backoffLimit, resources)
- Fix migration SA helper to require name when create=false
- Guard operator logic on both operator.enabled and migration.enabled
- Build and load operator image into kind for chart-testing CI
- Add path filters to operator workflow
- Fix ADR inaccuracies (retry strategy, default-enabled wording)
- Pin Dockerfile base image to golang:1.26.2
- Render extraInitContainers in operator mode (previously skipped)
- Add version label to migration Jobs and delete stale Jobs on image change
- Use namespaced Role/RoleBinding when watchAllNamespaces is false
- Replace Status().Update with Status().Patch to avoid write conflicts
- Fix logger.Error(nil, ...) to logger.Info for expected failure state
- Wire desiredVersion param into buildMigrationJob for version tracking
- Update RBAC: deployments/status verb from update to patch
- Don't force replicas: 0 for memory engine in operator mode
- Guard migration SA creation on migration.enabled
- Document both required labels and mutable tag limitation in README
- Add opt-in annotation (openfga.dev/migration-enabled) so the operator
  only manages migrations for explicitly opted-in Deployments
- Propagate volumes, volumeMounts, and envFrom from the Deployment to
  migration Jobs for TLS certs and file-based credentials
- Remove watchAllNamespaces option; operator is now always namespace-scoped
- Update ADR-004 dependency example to match actual file:// reference
- Add test for migration-not-enabled skip behavior
The migration Job should only receive explicitly filtered OPENFGA_DATASTORE_*
env vars, not the full EnvFrom from the source Deployment which could leak
non-datastore secrets.
- Wrap deployment annotations in conditional to avoid emitting empty
  annotations: field which produces an invalid manifest
- Store full version in annotation (openfga.dev/desired-version) and
  truncate label to 63 chars to support digest-pinned images
- Align operator image default to ghcr.io/openfga/openfga-operator to
  match CI publishing target
- Return error on retry-after annotation patch failure to prevent
  Job churn that bypasses the 60s cooldown
- Add test for stale-Job version mismatch deletion path
- Pin Dockerfile base images by digest for reproducible builds
- Fix version label fallback comparison for digest-pinned images
  by sanitizing desiredVersion before comparing to the label value
Replace checks on job.Status.Failed >= backoffLimit with
isJobConditionTrue(job, batchv1.JobFailed), and job.Status.Succeeded
with batchv1.JobComplete. The Job controller sets conditions atomically
when it makes its final decision, avoiding races where the operator
acts on intermediate counter states before Kubernetes has finished
cleaning up.
The old Helm-templated migration Job uses datastore.migrations.resources
for resource limits, but the operator-built Job had none. Inherit the
main container's Resources to maintain parity and prevent unbounded
resource consumption during migrations.
Four new tests covering previously untested code paths:

- StaleJob_LabelOnlyFallback: version-mismatch detection when Job has
  only a label (no annotation), exercising the sanitized-label fallback
- JobSucceeded_UpdatesExistingConfigMap: ConfigMap update path when a
  prior version's ConfigMap already exists
- ScaleToZero_NilAnnotationsMap: scaleDeploymentToZero correctly stores
  desired-replicas when the annotation was not previously set
- JobInProgress_Requeues: in-progress Job triggers 10s requeue without
  scaling up or modifying the Deployment
The test validates that scaleDeploymentToZero stores the current
replica count in the desired-replicas annotation before zeroing,
not that it handles a nil annotations map.
When concurrent reconciles race between the GET and CREATE, the second
create returns AlreadyExists. Treat this as benign and requeue to poll
the existing Job instead of returning a hard error that produces noisy
reconcile failures in the controller logs.
Address review findings: add allowPrivilegeEscalation: false for
restricted PSS compliance, set default resource requests/limits,
add values.schema.json validation, use stable selectorLabels on
pod template to prevent spurious rollouts, add .helmignore, and
improve Chart.yaml metadata and NOTES.txt with migration commands.
The operator was scaling Deployments to 0 replicas during every migration,
causing a full outage on every helm upgrade — a regression from the existing
rolling update behavior. OpenFGA already gates readiness on schema version
(MinimumSupportedDatastoreSchemaRevision in sqlcommon.IsReady), so new pods
naturally block until migration completes while old pods keep serving.

Use Helm's lookup function to preserve the live replica count on upgrade
(falling back to replicas: 0 on fresh install where no Deployment exists).
Remove scaleDeploymentToZero from the operator reconcile loop. Update
ADR-002 to document the rationale and the readiness gate dependency.
…ent template

- findOpenFGAContainer now reads the openfga.dev/container-name
  annotation emitted by the chart, and returns an error when the target
  container is missing instead of silently falling back to the first
  container in the pod spec.
- migration_controller surfaces that error to the reconciler instead
  of logging and skipping, so misconfigured Deployments are visible.
- deployment.yaml emits the new container-name annotation, collapses
  the replica-preservation logic to a single branch (both previous
  branches already preserved existing replicas), and uses
  selectorLabels on the pod template to avoid chart-version churn in
  pod labels across upgrades.
- values.yaml documents the openfga-operator subchart values
  passthrough and clarifies migration service account behavior.
…s stale, its JobComplete would write the wrong version into the status ConfigMap
The Job controller sets JobFailureTarget as soon as it decides a Job
will fail (backoff limit reached, active deadline exceeded, etc.) —
JobFailed only flips after pods finish terminating, which can take up
to BackoffLimit × ActiveDeadlineSeconds. Previously the operator only
watched JobFailed, so a broken migration took ~15 minutes (with chart
defaults) before MigrationFailed appeared on the Deployment.

Treat either condition as "failed" and add a regression test.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f473c923-1741-44f7-8db9-47dda256d945

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@socket-security

socket-security Bot commented Aug 24, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedsigs.k8s.io/​controller-runtime@​v0.23.373100100100100
Addedk8s.io/​apimachinery@​v0.35.374100100100100
Addedk8s.io/​client-go@​v0.35.37510010075100
Addedk8s.io/​api@​v0.35.376100100100100
Addedk8s.io/​utils@​v0.0.0-20260319190234-28399d86e0b588100100100100

View full report

@socket-security

socket-security Bot commented Aug 24, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: golang github.com/modern-go/reflect2 is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?golang/sigs.k8s.io/controller-runtime@v0.23.3golang/k8s.io/apimachinery@v0.35.3golang/k8s.io/client-go@v0.35.3golang/github.com/modern-go/reflect2@v1.0.3-0.20250322232337-35a7c28c31ee

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore golang/github.com/modern-go/reflect2@v1.0.3-0.20250322232337-35a7c28c31ee. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: golang k8s.io/client-go is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: operator/go.modgolang/k8s.io/client-go@v0.35.3

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore golang/k8s.io/client-go@v0.35.3. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn Medium
Medium CVE: Go Net HTML parser is vulnerable to denial of service in golang golang.org/x/net

CVE: GHSA-5cv4-jp36-h3mw Go Net HTML parser is vulnerable to denial of service (MODERATE)

Affected versions: < 0.55.0

Patched version: 0.55.0

From: ?golang/sigs.k8s.io/controller-runtime@v0.23.3golang/k8s.io/apimachinery@v0.35.3golang/k8s.io/client-go@v0.35.3golang/golang.org/x/net@v0.47.0

ℹ Read more on: This package | This alert | What is a medium CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Remove or replace dependencies that include known medium severity CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore golang/golang.org/x/net@v0.47.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@Siddhant-K-code Siddhant-K-code changed the title feat(operator): preserve authored migration controller history feat(operator): add migration orchestration controller Aug 24, 2026

Copilot AI 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.

Pull request overview

Adds a new operator/ Go module implementing a controller-runtime Kubernetes operator responsible for orchestrating OpenFGA datastore migrations via Jobs and recording completed versions in a status ConfigMap, including unit tests and build artifacts (Dockerfile/Makefile).

Changes:

  • Introduces MigrationReconciler to detect OpenFGA version changes, create/observe migration Jobs, update migration-status ConfigMaps, and manage retry cooldowns.
  • Adds controller-focused unit tests covering success/failure, stale resources, collisions, and retry behavior.
  • Adds operator module scaffolding and developer tooling (go.mod/go.sum, main entrypoint, Dockerfile, Makefile, README, .dockerignore).

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
operator/README.md Operator purpose, local development/testing instructions, flags, and limitations.
operator/Makefile Build/test/vet/fmt and docker build/push targets for the operator.
operator/internal/controller/migration_controller.go Core reconciliation logic for migrations, retries, and resource lifecycle.
operator/internal/controller/migration_controller_test.go Unit tests covering migration orchestration and edge cases.
operator/internal/controller/helpers.go Shared helpers for image parsing, job/configmap creation/update, scaling, and deletion preconditions.
operator/go.mod New Go module definition and dependencies for controller-runtime + Kubernetes APIs.
operator/go.sum Dependency checksums for the new module.
operator/Dockerfile Multi-stage, pinned-image build producing a distroless static operator image.
operator/cmd/main.go Operator manager setup, flags, cache scoping, and health endpoints.
operator/.dockerignore Docker build context exclusions for the operator directory.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread operator/internal/controller/migration_controller.go Outdated
Comment thread operator/internal/controller/migration_controller.go Outdated

Copilot AI 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.

Pull request overview

Copilot reviewed 9 out of 10 changed files in this pull request and generated 8 comments.

Comment thread operator/internal/controller/helpers.go Outdated
Comment thread operator/internal/controller/helpers.go Outdated
Comment thread operator/internal/controller/migration_controller.go
Comment thread operator/cmd/main.go Outdated
Comment thread operator/cmd/main.go Outdated
Comment thread operator/internal/controller/migration_controller.go Outdated
Comment thread operator/README.md Outdated
Comment thread operator/README.md Outdated

@SoulPancake SoulPancake left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

  • ensureDeploymentScaled enforces desired-replicas on every reconcile, not just after migration. Any kubectl scale is instantly reverted (the scale event itself triggers the reconcile), and with autoscaling blocked in operator mode that's the only scaling left. Also snaps back the live replica count the chart's lookup deliberately preserves, right after the Job succeeds. Make it a one-shot gate — only scale when replicas is 0 — and add a test that a non-zero live count is left alone.
  • Stale-version/legacy-hook Jobs are deleted with background propagation while possibly still running, and the replacement lands ~5s later → two concurrent openfga migrate runs while the old pod terminates. Foreground propagation fixes it: the Job lingers until pods are gone, so recreation waits naturally.

Nits: cmd/main.go fails gofmt -l; module path should be github.com/openfga/helm-charts/operator (operator lives here per ADR-003) unless extraction is planned; updateMigrationStatus overwrites existing.Labels

Make migration Job replacement deletion-safe, preserve stable Helm adoption across chart upgrades, and avoid status or replica churn. Harden startup validation and namespace scoping, preserve ConfigMap labels, and use the repository-local module path.
@Siddhant-K-code

Copy link
Copy Markdown
Member Author

Addressed the controller review in d0e22e5d.

  • Replica restoration is now a one-shot zero-replica gate with optimistic locking; non-zero live counts are left untouched.
  • Migration Job deletion uses foreground propagation and waits for termination before replacement.
  • Go formatting is clean, and the module path is now github.com/openfga/helm-charts/operator.
  • Migration status updates preserve unrelated ConfigMap labels while enforcing required labels.

The same commit also fixes the namespace-scope fallback, active-deadline validation, status-patch durability, idempotent failure-condition clearing, and cross-chart-version legacy Job adoption. Full tests, race tests, vet, and build pass.

Copilot AI 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.

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 8 comments.

Suppressed comments (1)

operator/internal/controller/migration_controller.go:217

  • This status merge patch can silently replace concurrently updated Deployment conditions because it does not include a resource-version precondition. Use an optimistic-lock merge patch and let a conflict retry the condition clear.
		statusPatch := client.MergeFrom(deployment.DeepCopy())

Comment thread operator/internal/controller/migration_controller.go
Comment thread operator/internal/controller/helpers.go Outdated
Comment thread operator/internal/controller/migration_controller.go Outdated
Comment thread operator/internal/controller/migration_controller.go Outdated
Comment thread operator/internal/controller/helpers.go
Comment thread operator/internal/controller/migration_controller.go Outdated
Comment thread operator/internal/controller/migration_controller.go Outdated
Comment thread operator/README.md
Persist terminal results before arming cleanup, retain failed Jobs through cooldown and diagnostics, and serialize replacement behind foreground deletion. Repair status ownership, lock condition patches, preserve pull policy, and revalidate controller labels.
@Siddhant-K-code

Copy link
Copy Markdown
Member Author

Closing this stack for now. We're going to focus first on updating, reviewing, and fixing #309.

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