Rewrite/watch manager - #340
Open
Tanker2020 wants to merge 44 commits into
Open
Conversation
- Node: name, optional NodeFunc, directed edges to upstream deps - Edge: carries optional EdgeFunc to gate dependent start - ResourceNode: embeds Node, adds Manifest/VerifyFunc/DeployMethod - Graph: root-anchored container; AddNode, AddDependency, Topology - Cycle detection on every AddChild call (DFS reachability) - Topology() returns DFS post-order (dependency-first deploy order) Files: rewrite/dag/node.go, rewrite/dag/graph.go Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
- CompletionState: Verified/Unverified/Failed/Unstarted node buckets
- DeployCompleted(), VerifyCompleted(), AnyFailed() predicates
- HaltError{Fatal bool}: returned by NodeFunc to signal runner halt
Fatal=true → node lands in Failed, downstreams become Unstarted
Fatal=false → node lands in Unverified (deployed, not yet ready)
Files: rewrite/dag/completion_state.go
Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
Python used ThreadPoolExecutor + time.sleep(0.05) busy-poll loop. Go port uses goroutines + buffered results channel; scheduler blocks on select — zero CPU busy-polling. - NewRunner(graph, opts...) with functional options - WithConcurrency(0): serial topology walk, no goroutines (dry-run/test) - WithConcurrency(n): semaphore-capped parallel execution - WithVerifyUpstream(bool): gate dependent start on EdgeFunc result - context.Context cancellation: drains in-flight, marks rest Unstarted - Independent graph branches continue executing after sibling failure (matches Python oper8 intended behaviour; Python had a bug where the serial loop broke early on fatalErr) - stateMap protected by sync.Mutex; scheduler is single writer Files: rewrite/dag/runner.go Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
23 test cases covering:
Graph/Node: empty graph, duplicate node, empty name, cycle detection,
self-loop, topology order, String()
Runner serial: all succeed, empty graph, fatal halt (independent branch
still runs), unverified halt, disabled node, execution order
Runner concurrent: all succeed, fatal halt, independent nodes verified
parallel via start-time spread, race detector stress test
(20 nodes, atomic counter), context cancellation
EdgeFunc: blocks dependent when returns false, allows when true
CompletionState: all predicate combinations
ResourceNode: construction and field access
Concurrency test uses start-time recording rather than wall-clock
total elapsed — CI-safe on slow runners.
Files: rewrite/dag/runner_test.go
Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
- Matrix: Go 1.22 and 1.23 - go test -race -count=1 -timeout=60s ./dag/... - go build ./... and go vet ./dag/... - golangci-lint on dag/ package - Triggered on push to rewrite/DAG_Runner and PRs targeting main - working-directory: rewrite (module root) Files: ./.github/workflows/pr1-dag-runner.yml Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
Defines the core abstraction all cluster interactions go through. Python (bool, bool) return tuples → Go (changed bool, err error): - success bool dropped; errors are returned as error values - callers use idiomatic `if err != nil` instead of checking two booleans watch_objects Python generator → Go channel: - Watch() returns <-chan WatchEvent; caller ranges over it - Cancelled via context.Context; channel is closed on cancel New types vs Python: - ListOptions struct (replaces positional label_selector/field_selector args) - EventType string constants (ADDED/MODIFIED/DELETED) - WatchEvent struct with Timestamp Files: rewrite/deploymanager/deploymanager.go Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
Ports deploy_manager/owner_references.py. - OwnerRef(ownerCR) builds a single ownerReference map entry - ApplyOwnerRef(owner, child) stamps the reference onto child.metadata - No-op when owner == child (same UID) - No-op for cross-namespace references (K8s does not support them) - Idempotent: will not add duplicate entries - blockOwnerDeletion: true; controller field intentionally omitted (matches Python behaviour and StackOverflow rationale in source) Files: rewrite/deploymanager/ownerref.go Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
Ports deploy_manager/dry_run_deploy_manager.py. Primary use: unit-testing controllers without a live cluster. Key differences from Python: - Python used nested defaultdict; Go uses typed clusterStore (map[ns][kind][apiVersion][name] → object) - Python RLock on class level; Go sync.RWMutex per instance - Python watch callbacks were registered functions; Go uses buffered channels — consumers range over the channel, cancel via context - Watch channel is closed when ctx is cancelled (no explicit Unregister) - deepCopy via JSON marshal/unmarshal (simple, correct for map[string]any) - matchSelector implements = == != existence operators (sufficient for dry-run tests; full set-based selector is future work) Extra test helpers (not in Python): - GetStored(ns, kind, av, name) — direct store access for assertions - ObjectCount() — total objects in store Files: rewrite/deploymanager/dryrun.go Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
19 test cases covering:
Deploy: create, idempotent re-deploy, field update, owner ref stamping
Get: not found returns nil, found returns deep copy (mutation check)
Delete: existing object, non-existent no-op
List: all objects, label selector filtering
SetStatus: sets status, returns changed=true; error on missing object
Watch: receives ADDED on deploy, DELETED on delete, channel closes
on context cancel (race-detector safe)
OwnerRef: stamps reference, idempotent, cross-namespace skipped
Files: rewrite/deploymanager/dryrun_test.go
Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
- Matrix: Go 1.22 and 1.23 - go test -race -count=1 -timeout=60s ./deploymanager/... - go build ./... and go vet ./deploymanager/... - golangci-lint on deploymanager/ package - Triggered on push to rewrite/Deploy_Manager and PRs targeting main Files: .github/workflows/pr2-deploy-manager.yml Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
…licationStatus Ports oper8 Python status.py to Go. Reason types: ReadyReason, UpdatingReason, ServiceStatus string constants. MakeApplicationStatus(Options) builds a complete status map: - Ready + Updating conditions from reason/message pairs - External conditions preserved alongside oper8-managed ones - ComponentStatus block from dag.CompletionState (sorted node names) - versions.reconciled / versions.available.versions (IBM CloudPak paths) - <kind>Status field (e.g. customerStatus) when Kind is set UpdateApplicationStatus merges new Options onto existing status, carrying forward current reasons and external conditions when not overridden. GetCondition, GetVersion, StatusChanged helper functions included. Python translation notes: - deepdiff library dropped; StatusChanged uses recursive JSON comparison after stripping lastTransactionTime keys — zero external dependencies - **kwargs replaced by Options struct (compile-time field checking) - aconfig nested_set/nested_get replaced by nestedSet/nestedGet dot-path helpers Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
MakeApplicationStatus: Ready/Updating condition status values for all reason combinations, empty options, external conditions, version fields, componentStatus deployed/verified counts and dependencyGraph, IBM CloudPak <kind>Status (Completed/Failed/InProgress/custom preserved) UpdateApplicationStatus: preserves existing reasons when not overridden, overrides when provided, preserves external conditions and top-level fields StatusChanged: same content + different timestamps not changed, different reason changed, nil inputs, added field GetCondition, GetVersion: found/missing cases UpdatingReason active/inactive matrix (all 6 reasons) ComponentStatus node names sorted alphabetically Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
Matrix Go 1.22 and 1.23, race detector, golangci-lint v1.64.8 Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
…load config Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
…ondition Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
…rity tests Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
…r, ReconcileManager
Ports oper8 Python session.py / component.py / controller.py /
rollout_manager.py / reconcile.py to Go.
New packages
rewrite/session/ per-reconcile context (CR manifest, DAG, status)
rewrite/component/ Component interface (Setup / Deploy / Verify)
rewrite/controller/ Controller interface + BaseController no-op embed
rewrite/rolloutmanager/ 4-phase loop: deploy→after_deploy→verify→after_verify
rewrite/reconcilemanager/ top-level orchestrator: ID gen, session init,
preconditions, rollout, status writes, finalizers
dag/node.go Node.SetFunc / SetData / Data (needed by RolloutManager)
dag/runner.go Runner.CompletionState(); remove unused inFlight int64 field
gofmt dag/runner_test.go, deploymanager/dryrun.go
reconcilemanager tests (9 cases, -race):
EmptyGraph, SingleComponentVerified, SetupError, DeployError,
VerifyNotReady, Precondition, TwoComponentsOrdered, InvalidCR, Finalizer
CI: .github/workflows/pr5-reconcile.yml — go test -race ./... + golangci-lint
Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
dag/runner.go: Remove unused struct field — this was the field golangci-lint flagged. The concurrent scheduler uses a local variable inside runConcurrent; the struct field was never read or written and should never have been there. session/session_test.go: 21 tests (lost in branch switch, recreated) controller/controller_test.go: 14 tests (lost in branch switch, recreated) rolloutmanager/rolloutmanager_test.go: 18 tests (lost in branch switch, recreated) Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
…0:00) nishanthk ttys002 Thu Jul 30 11:30 - 11:30 (00:00) nishanthk ttys002 Thu Jul 30 11:30 - 11:30 (00:00) nishanthk ttys002 Thu Jul 30 11:30 - 11:30 (00:00) nishanthk ttys002 Thu Jul 30 11:30 - 11:30 (00:00) nishanthk ttys002 Thu Jul 30 11:29 - 11:29 (00:00) nishanthk ttys002 Thu Jul 30 11:29 - 11:29 (00:00) nishanthk ttys002 Thu Jul 30 11:28 - 11:28 (00:00) nishanthk ttys002 Thu Jul 30 11:23 - 11:23 (00:00) nishanthk ttys001 Thu Jul 30 11:19 still logged in nishanthk ttys000 Thu Jul 30 11:19 still logged in nishanthk console Thu Jul 30 11:19 still logged in reboot time Thu Jul 30 11:00 shutdown time Thu Jul 30 10:59 nishanthk ttys001 Fri Jul 24 15:26 - 15:26 (00:00) nishanthk ttys000 Fri Jul 24 15:26 - 15:26 (00:00) nishanthk console Fri Jul 24 15:26 - 10:59 (5+19:33) reboot time Fri Jul 24 15:25 shutdown time Fri Jul 24 15:24 nishanthk ttys001 Wed Jul 22 00:37 - 00:37 (00:00) nishanthk ttys000 Wed Jul 22 00:37 - 00:37 (00:00) nishanthk console Wed Jul 22 00:37 - 15:24 (2+14:47) reboot time Wed Jul 22 00:35 shutdown time Wed Jul 22 00:31 root console Wed Jul 22 00:30 - shutdown (00:00) nishanthk ttys001 Fri Jul 17 16:28 - 16:28 (00:00) nishanthk ttys001 Thu Jul 9 13:25 - 13:25 (00:00) nishanthk ttys001 Mon Jul 6 14:14 - 14:14 (00:00) nishanthk ttys005 Mon Jul 6 14:14 - 14:14 (00:00) nishanthk ttys006 Mon Jul 6 14:14 - 14:14 (00:00) nishanthk ttys003 Mon Jun 29 14:47 - 14:47 (00:00) nishanthk ttys002 Mon Jun 29 14:47 - 14:47 (00:00) nishanthk ttys001 Mon Jun 29 14:47 - 14:47 (00:00) nishanthk ttys006 Mon Jun 29 14:21 - 14:21 (00:00) nishanthk ttys006 Mon Jun 29 14:21 - 14:21 (00:00) nishanthk ttys003 Mon Jun 29 13:36 - 13:36 (00:00) nishanthk ttys005 Mon Jun 22 02:32 - 02:32 (00:00) nishanthk ttys004 Mon Jun 22 02:30 - 02:30 (00:00) nishanthk ttys003 Mon Jun 22 01:16 - 01:16 (00:00) nishanthk ttys002 Thu Jun 18 16:23 - 16:23 (00:00) nishanthk ttys001 Thu Jun 18 16:23 - 16:23 (00:00) nishanthk ttys000 Thu Jun 18 16:23 - 16:23 (00:00) nishanthk console Thu Jun 18 16:23 - 00:30 (33+08:07) reboot time Thu Jun 18 16:23 nishanthk ttys002 Wed Jun 17 12:32 - crash (1+03:50) nishanthk ttys001 Mon Jun 15 13:30 - crash (3+02:52) nishanthk ttys000 Mon Jun 15 13:30 - crash (3+02:53) nishanthk ttys003 Mon Jun 15 13:17 - 13:17 (00:00) nishanthk ttys003 Mon Jun 15 13:17 - 13:17 (00:00) nishanthk ttys002 Mon Jun 15 13:16 - 13:16 (00:00) nishanthk ttys003 Mon Jun 15 13:14 - 13:14 (00:00) nishanthk ttys002 Mon Jun 15 13:12 - 13:12 (00:00) nishanthk ttys002 Mon Jun 15 13:11 - 13:11 (00:00) nishanthk ttys003 Mon Jun 15 13:07 - 13:07 (00:00) nishanthk ttys002 Fri Jun 12 13:02 - 13:02 (00:00) nishanthk ttys002 Thu Jun 11 12:44 - 12:44 (00:00) nishanthk ttys004 Mon Jun 8 13:56 - 13:56 (00:00) nishanthk ttys002 Mon Jun 8 13:56 - 13:56 (00:00) nishanthk ttys003 Mon Jun 8 13:56 - 13:56 (00:00) nishanthk ttys004 Fri Jun 5 09:34 - 09:34 (00:00) nishanthk ttys002 Fri Jun 5 09:34 - 09:34 (00:00) nishanthk ttys003 Fri Jun 5 09:34 - 09:34 (00:00) nishanthk ttys020 Thu Jun 4 11:20 - 11:20 (00:00) nishanthk ttys004 Wed Jun 3 16:42 - 16:42 (00:00) nishanthk ttys003 Wed Jun 3 00:06 - 00:06 (00:00) nishanthk ttys002 Wed Jun 3 00:06 - 00:06 (00:00) nishanthk ttys001 Wed Jun 3 00:06 - 00:06 (00:00) nishanthk ttys000 Wed Jun 3 00:06 - 00:06 (00:00) nishanthk console Wed Jun 3 00:06 - crash (15+16:17) reboot time Wed Jun 3 00:04 shutdown time Tue Jun 2 23:59 root console Tue Jun 2 23:57 - shutdown (00:02) nishanthk ttys003 Mon Jun 1 21:50 - 21:50 (00:00) nishanthk ttys002 Mon Jun 1 21:50 - 21:50 (00:00) nishanthk ttys001 Mon Jun 1 21:50 - 21:50 (00:00) nishanthk ttys000 Mon Jun 1 21:50 - 21:50 (00:00) nishanthk console Mon Jun 1 21:50 - 23:57 (1+02:07) reboot time Mon Jun 1 21:49 shutdown time Mon Jun 1 21:49 root console Mon Jun 1 21:49 - shutdown (00:00) nishanthk ttys003 Mon Jun 1 10:57 - 10:57 (00:00) nishanthk ttys009 Mon Jun 1 10:21 - 10:21 (00:00) nishanthk ttys000 Mon Jun 1 10:15 - 10:15 (00:00) nishanthk ttys004 Fri May 29 15:11 - 15:11 (00:00) nishanthk ttys003 Fri May 29 12:56 - 12:56 (00:00) nishanthk ttys002 Wed May 27 16:07 - 16:07 (00:00) nishanthk ttys001 Wed May 27 16:05 - 16:05 (00:00) nishanthk ttys001 Wed May 27 11:31 - 11:32 (00:00) nishanthk ttys001 Wed May 27 11:31 - 11:31 (00:00) nishanthk ttys002 Tue May 26 17:36 - 17:36 (00:00) nishanthk ttys001 Tue May 26 17:24 - 17:24 (00:00) nishanthk ttys001 Tue May 26 15:48 - 15:48 (00:00) nishanthk ttys001 Tue May 26 15:47 - 15:47 (00:00) nishanthk ttys000 Tue May 26 15:46 - 15:46 (00:00) nishanthk ttys001 Tue May 26 15:42 - 15:42 (00:00) nishanthk ttys001 Tue May 26 15:31 - 15:31 (00:00) nishanthk ttys000 Tue May 26 14:44 - 14:44 (00:00) nishanthk ttys002 Tue May 26 14:43 - 14:43 (00:00) nishanthk ttys002 Tue May 26 14:43 - 14:43 (00:00) nishanthk ttys002 Tue May 26 14:43 - 14:43 (00:00) nishanthk ttys002 Tue May 26 14:43 - 14:43 (00:00) nishanthk ttys002 Tue May 26 14:43 - 14:43 (00:00) nishanthk ttys002 Tue May 26 14:43 - 14:43 (00:00) nishanthk ttys002 Tue May 26 14:43 - 14:43 (00:00) nishanthk ttys002 Tue May 26 14:43 - 14:43 (00:00) nishanthk ttys002 Tue May 26 14:43 - 14:43 (00:00) nishanthk ttys002 Tue May 26 14:43 - 14:43 (00:00) nishanthk ttys002 Tue May 26 14:43 - 14:43 (00:00) nishanthk ttys001 Tue May 26 14:42 - 14:42 (00:00) nishanthk ttys000 Tue May 26 14:31 - 14:31 (00:00) nishanthk console Tue May 26 13:42 - 21:49 (6+08:07) _mbsetupuser console Tue May 26 13:27 - 13:42 (00:14) root console Tue May 26 13:27 - 13:27 (00:00) reboot time Tue May 26 13:26 shutdown time Thu May 21 02:12 reboot time Thu May 21 02:03 reboot time Tue Mar 3 22:23 reboot time Tue Mar 3 22:18 wtmp begins Tue Mar 3 22:18:14 CST 2026 struct field to pass golangci-lint The field on Runner was written but never read externally — Run() already returns *CompletionState directly. The linter correctly flags any struct field that is never read. Removing it fixes the CI lint failure. Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
…efault - Add Disabled() bool to Component interface; RolloutManager skips Setup/Deploy/Verify for disabled components (no-op DAG success) - Fix requeue: ReconcileManager now uses !VerifyCompleted()||ShouldRequeue instead of ShouldRequeue alone; BaseController.ShouldRequeue → false - Fix addFinalizer/removeFinalizer: DeployMethodUpdate → DeployMethodDefault (existing-wins merge was silently dropping the mutated finalizer list) - Expand reconcilemanager tests 9→24 cases; add 2 disabled-component tests to rolloutmanager Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
… dead code
- rolloutmanager: fix append aliasing bug in CompletionState assembly;
deployedAll now uses make+copy instead of append on a shared backing array
- rolloutmanager: remove redundant second unverifiedNodes loop (verifyState.Unverified
is a strict subset of deployedAll; second loop could never add anything)
- rolloutmanager: correct stale package doc (Disabled() is now first-class)
- reconcilemanager: fix nil-map panic in updateCompletionStatus; GetCondition
returns nil on a fresh CR — guard before map index
- reconcilemanager: fix ManageStatus doc comment ("Default true" → zero value is false)
- controller: fix ShouldRequeue interface doc (claimed "(true, 0)" multi-return
for a method that returns bool)
- reconcilemanager_test: collapse two duplicate RequeueAfter tests into one;
remove time import kept alive only by _ = time.Second
Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
… Makefile, CI Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
… Makefile, CI Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
…ests Port all remaining oper8 Python modules to Go. Replace ~900-line WatchManager with a ~200-line controller-runtime adapter. Add comprehensive test suite (306 tests, go test -race ./... clean). Packages ported - constants: all annotation keys, PassthroughAnnotations, misc constants - errors: 5-type hierarchy (ConfigError/ClusterError/RolloutError fatal; PreconditionError/VerificationError transient); IsFatal() + assert helpers - utils: MergeConfigs (deep), GetNested/SetNested (dotted keys), GetPassthroughAnnotations - status: MakeApplicationStatus, UpdateApplicationStatus, StatusChanged (timestamp-ignoring diff), GetVersion, IBM CloudPak kind field - dag: Graph, Node, concurrent+serial Runner, HaltError, CompletionState; verified race-safe under go test -race - session: per-reconcile context, CR validation, ScopedName/TruncateName - component: Component interface (Name/Disabled/Setup/Deploy/Verify) - controller: Controller interface + BaseController (all hook no-ops) - deploymanager: DeployManager interface; DryRunDeployManager (thread-safe, watch events); OwnerRef/ApplyOwnerRef - deploymanager/k8s: production client (SSA default, Update, Replace, Delete, Get, List, SetStatus); SSA→Create fallback for fake client in tests - rolloutmanager: 4-phase rollout (deploy, after-deploy, verify, after-verify) - reconcilemanager: full reconcile lifecycle (ID, session, finalizer, preconditions, setup/finalize, rollout, status, requeue) - verify: VerifyResource, VerifyPod/Job/Deployment/StatefulSet/Subsystem, kind registry, condition sort, per-call VerifyFunc, custom timestamp key - patch: Apply() with SMP (typed schema) + JSON-6902; component-name routing - temporarypatch: Component (patch annotation add/remove) + Controller (finalizer, patchable-kinds allowlist) - watchmanager: Adapter (implements reconcile.Reconciler), predicates (GenerationChangedOrDeleted, NotPaused), GVKFromString - cmd/run.go: RunOperator() — wires manager, probes, leader election WatchManager delta vs Python - Python: ~900 lines (custom watch/queue/leader-election/backoff/isolation) - Go: ~200 lines (delegates everything to controller-runtime) - Pause filter moved from inside reconcile to predicate layer (never enqueued) - Generation filter fixed: passes spec changes AND deletions with same gen - Subprocess isolation removed: goroutines + -race make it unnecessary Bug fixes - errors.IsFatal(): type-assert was *Oper8Error but concrete types are *ConfigError etc.; fixed to interface check - patch.resolvePatchPayload: routing semantics corrected (nil on no-match) - TestApply_UnsupportedPatchType: empty-map payload bypassed type-switch; test corrected to use a resolvable payload New test files: constants/constants_test.go, component/component_test.go Expanded: errors, patch, dag, session, verify (+67 tests across 5 packages) Total: 306 tests, 16 packages, go test -race ./... Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
Signed-off-by: Nishanth Kolakalapudi <Nishanth.Kol@ibm.com>
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.
PR-6: Go rewrite — WatchManager, full operator lifecycle, and comprehensive test suite
Summary
This PR completes the Go rewrite of oper8's core operator framework, replacing ~3,000 lines of Python with idiomatic, race-safe Go that delegates watch management, leader election, work-queuing, and health probes entirely to controller-runtime. All 18 packages now compile; 306 tests pass under
go test -race ./....What was ported from Python
constantsconstants.pyPassthroughAnnotations, misc constantserrorsexceptions.pyConfigError,ClusterError,RolloutError(fatal);PreconditionError,VerificationError(transient);IsFatal()+ assert helpersutilsutils.pyMergeConfigs,GetNested,SetNested,GetPassthroughAnnotationsstatusstatus.pyMakeApplicationStatus,UpdateApplicationStatus,StatusChanged, condition/version helpersdagdag/node.py+dag/graph.pyNode,Graph,Runner(concurrent + serial),HaltError,CompletionStatesessionsession.pyScopedName/TruncateNamecomponentcomponent.pyComponentinterface (Name, Disabled, Setup, Deploy, Verify)controllercontroller.pyControllerinterface,BaseController(all hook no-ops),GVK,HookResultdeploymanagerdeploy_manager/base.pyDeployManagerinterface,DryRunDeployManager(in-memory, thread-safe, watch events),OwnerRef/ApplyOwnerRefdeploymanager/k8sk8s.Client: SSA default, Update, Replace, Delete, Get, List, SetStatusrolloutmanagerrollout_manager.pyreconcilemanagerreconcile.pyverifyverify_resources.pyVerifyResource,VerifyPod,VerifyJob,VerifyDeployment,VerifyStatefulSet,VerifySubsystem, kind registrypatchpatch.py+patch_strategic_merge.pyApply()with SMP (typed schema for Deployment/StatefulSet/etc.) + JSON-6902 (RFC 6902), component-name routingtemporarypatchtemporary_patch/Component(add/remove patch annotation on target CR),Controller(finalizer, patchable-kinds allowlist)watchmanagerwatch_manager.pycmd/run.gocmd/run_operator_cmd.pyRunOperator(Options): wires manager, predicates, health/readyz probes, leader election, signal handlingWatchManager: from ~900 Python lines to ~200 Go lines
The Python
WatchManagerwas the largest and most complex module — it implemented its own watch streams, work queue, rate limiting, leader election, subprocess-per-reconcile isolation, requeue backoff, and health probes, all from scratch.In Go, controller-runtime provides all of that natively. The
watchmanagerpackage is now a thin adapter of ~200 lines:threading.Queue+ customctrl.Options{LeaderElection: true}mgr.AddHealthzCheck/AddReadyzCheckNotPausedpredicate — never enqueuedGenerationChangedOrDeletedpredicate — gen change orDeletionTimestampNew capabilities not in the Python version
sync.Mutex, verified clean undergo test -raceNotPausedandGenerationChangedOrDeletedprevent spurious objects from ever reaching the reconcile queueGVKrouting —schema.GroupVersionKindstructs everywhere;GVKFromStringvalidates format at startupDryRunDeployManager— fully thread-safe in-memory cluster simulator with watch event emission; used in all unit tests; no cluster neededBugs fixed during this PR
errors.IsFatal()always returnedfalsefor fatal errors — type-assertederr.(*Oper8Error)but concrete types are*ConfigError/*ClusterErroretc. (embedded structs). Fixed by asserting against afatalCheckerinterface.patch.resolvePatchPayloadrouting was inverted — patch entries without a matching component-name key were silently skipped instead of returningnil. Tests corrected to nest patch payloads under theinternalNamerouting key, matching Python semantics.TestApply_UnsupportedPatchTypenever exercised the switch — the test passed an empty map payload; the resolver found no key and returnednil, bypassing the type-switch entirely. Test fixed to use a resolvable payload.Test coverage
306 tests across 16 packages (all passing,
go test -race ./...):watchmanagerreconcilemanagerrolloutmanagerdeploymanager/k8sdeploymanagerdagHaltError.Unwrap,GetNode,Nodes()root exclusionstatusverifysessionpatcherrorscontrollerutilsconstantscomponenttemporarypatchWhat still needs to be done (follow-on PRs)
make generate— runcontroller-gento replace the hand-writtenzz_generated.deepcopy.gostubmake manifests— populateconfig/crd/bases/with real CRD YAML (needed by OLM and integration tests)cmd/run.gotests — smoke test forRunOperator()option validation and manager wiringgo.modstabilisation — an external process keeps bumping to alpha k8s deps; pin tov0.31.0stable once resolvedNamespaces []stringthroughwatchmanager.Optionsintoctrl.Options{Cache: ...}for namespace-restricted operatorsHow to test