Skip to content

Rewrite/deploy manager - #334

Open
Tanker2020 wants to merge 18 commits into
IBM:mainfrom
Tanker2020:rewrite/Deploy_Manager
Open

Rewrite/deploy manager#334
Tanker2020 wants to merge 18 commits into
IBM:mainfrom
Tanker2020:rewrite/Deploy_Manager

Conversation

@Tanker2020

Copy link
Copy Markdown

Summary

Ports the Python oper8 deploy manager layer to Go. Provides the DeployManager interface (the single point of contact between operator components and the Kubernetes cluster) along with a fully in-memory DryRunDeployManager for unit testing, and owner-reference helpers.

Files changed:

  • rewrite/deploymanager/deploymanager.goDeployManager interface + types
  • rewrite/deploymanager/ownerref.goOwnerRef, ApplyOwnerRef
  • rewrite/deploymanager/dryrun.goDryRunDeployManager
  • rewrite/deploymanager/dryrun_test.go — 19 table-driven tests
  • rewrite/.golangci.yml — shared lint config for all rewrite PRs
  • .github/workflows/pr2-deploy-manager.yml — CI

Depends on: PR-1 (dag/ package) — stacked branch, but the deploy manager package itself does not import dag/. PR-2 can be reviewed independently of PR-1.


What was ported

Python Go equivalent
deploy_manager/base.pyDeployManagerBase, DeployMethod deploymanager/deploymanager.go
deploy_manager/kube_event.pyKubeWatchEvent, KubeEventType deploymanager/deploymanager.go (WatchEvent, EventType)
deploy_manager/owner_references.pyupdate_owner_references deploymanager/ownerref.go (OwnerRef, ApplyOwnerRef)
deploy_manager/dry_run_deploy_manager.pyDryRunDeployManager deploymanager/dryrun.go

Design decisions

Abstract base class → Go interface

Python used DeployManagerBase(abc.ABC) with @abc.abstractmethod decorators. 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) and changed (did the cluster state change). Go collapses these: if there is an error, the operation failed; the single return value is changed. Callers use the standard if err != nil pattern.

watch_objects generator → <-chan WatchEvent

Python's watch_objects was a generator/iterator. Go returns a <-chan WatchEvent that receives events until the passed context.Context is cancelled, at which point the channel is closed. The caller ranges over it naturally.

DryRunDeployManager cluster store

Python used a nested defaultdict. Go uses an explicit type alias clusterStore = map[string]map[string]map[string]map[string]map[string]any (keyed: namespace → kind → apiVersion → name → object). All mutations are protected by a single sync.RWMutex making the implementation safe under -race.

disable()Delete()

Python named the delete method disable(). The Go interface names it Delete() to match Kubernetes/controller-runtime conventions and make the semantics unambiguous.

Deep copy via JSON round-trip

Get() and List() return deep copies so callers cannot accidentally mutate the store. The copy is done via json.Marshal + json.Unmarshal — simple, correct for map[string]any, no external dependency.

strict_resource_version / generate_resource_version not ported

Python's DryRunDeployManager had optional strict_resource_version and generate_resource_version modes 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)

Area Tests
Deploy create (changed=true), idempotent re-deploy (changed=false), field update (changed=true)
Get not found (nil, nil), found, returns deep copy
Delete existing (changed=true), non-existent (changed=false)
List all objects, label selector filtering
SetStatus sets status, not-found error
Watch receives ADDED event on deploy, receives DELETED event on delete, channel closes on context cancel
OwnerRef stamps reference, idempotent (no duplicate), cross-namespace skipped, Deploy with manageOwnerRefs=true

All tests run with -race.

- 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
Tanker2020 marked this pull request as ready for review August 13, 2026 16:34
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.

1 participant