Rewrite/deploy manager - #334
Open
Tanker2020 wants to merge 18 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>
…load config 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>
Tanker2020
marked this pull request as ready for review
August 13, 2026 16:34
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.
Summary
Ports the Python
oper8deploy manager layer to Go. Provides theDeployManagerinterface (the single point of contact between operator components and the Kubernetes cluster) along with a fully in-memoryDryRunDeployManagerfor unit testing, and owner-reference helpers.Files changed:
rewrite/deploymanager/deploymanager.go—DeployManagerinterface + typesrewrite/deploymanager/ownerref.go—OwnerRef,ApplyOwnerRefrewrite/deploymanager/dryrun.go—DryRunDeployManagerrewrite/deploymanager/dryrun_test.go— 19 table-driven testsrewrite/.golangci.yml— shared lint config for all rewrite PRs.github/workflows/pr2-deploy-manager.yml— CIDepends on: PR-1 (
dag/package) — stacked branch, but the deploy manager package itself does not importdag/. PR-2 can be reviewed independently of PR-1.What was ported
deploy_manager/base.py—DeployManagerBase,DeployMethoddeploymanager/deploymanager.godeploy_manager/kube_event.py—KubeWatchEvent,KubeEventTypedeploymanager/deploymanager.go(WatchEvent,EventType)deploy_manager/owner_references.py—update_owner_referencesdeploymanager/ownerref.go(OwnerRef,ApplyOwnerRef)deploy_manager/dry_run_deploy_manager.py—DryRunDeployManagerdeploymanager/dryrun.goDesign decisions
Abstract base class → Go interface
Python used
DeployManagerBase(abc.ABC)with@abc.abstractmethoddecorators. Go uses a plain interface. No base struct, no embedded types. Every implementation must satisfy all six methods — the compiler enforces it.(success, changed)→(changed bool, err error)Python returned two booleans:
success(did the operation not error) andchanged(did the cluster state change). Go collapses these: if there is an error, the operation failed; the single return value ischanged. Callers use the standardif err != nilpattern.watch_objectsgenerator →<-chan WatchEventPython's
watch_objectswas a generator/iterator. Go returns a<-chan WatchEventthat receives events until the passedcontext.Contextis cancelled, at which point the channel is closed. The callerranges over it naturally.DryRunDeployManagercluster storePython used a nested
defaultdict. Go uses an explicit type aliasclusterStore = map[string]map[string]map[string]map[string]map[string]any(keyed: namespace → kind → apiVersion → name → object). All mutations are protected by a singlesync.RWMutexmaking the implementation safe under-race.disable()→Delete()Python named the delete method
disable(). The Go interface names itDelete()to match Kubernetes/controller-runtime conventions and make the semantics unambiguous.Deep copy via JSON round-trip
Get()andList()return deep copies so callers cannot accidentally mutate the store. The copy is done viajson.Marshal+json.Unmarshal— simple, correct formap[string]any, no external dependency.strict_resource_version/generate_resource_versionnot portedPython's
DryRunDeployManagerhad optionalstrict_resource_versionandgenerate_resource_versionmodes used for testing optimistic concurrency. These are not needed for the current component/controller tests and are left for a future PR if required.Test coverage (19 tests)
All tests run with
-race.