refactor: remove Kubernetes content (moved to rodin/kubernetes-patterns)
This commit is contained in:
@@ -1,17 +1,15 @@
|
|||||||
# Go Patterns
|
# Go Patterns
|
||||||
|
|
||||||
Idiomatic Go patterns extracted from the [Go standard library](https://github.com/golang/go) and [Kubernetes](https://github.com/kubernetes/kubernetes) source code with verified file:line citations.
|
Idiomatic Go patterns extracted from the [Go standard library](https://github.com/golang/go) source code with verified file:line citations.
|
||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
- `patterns/` — Go stdlib patterns (interfaces, errors, concurrency, structs, testing, docs, style, API conventions, packages)
|
- `patterns/` — Go stdlib patterns (interfaces, errors, concurrency, structs, testing, docs, style, API conventions, packages)
|
||||||
- `kubernetes/` — Production-scale patterns from Kubernetes (controllers, informers, workqueues)
|
|
||||||
- `comparison/` — stdlib vs Kubernetes patterns
|
|
||||||
- `smells/` — Anti-patterns and common Go mistakes
|
- `smells/` — Anti-patterns and common Go mistakes
|
||||||
- `changelog/` — Daily digest of merged PRs
|
- `changelog/` — Daily digest of merged Go PRs
|
||||||
|
|
||||||
## Philosophy
|
## Philosophy
|
||||||
|
|
||||||
These rules are derived from what the Go source code actually does, not opinions or blog posts. Every pattern cites specific files and line numbers.
|
These rules are derived from what the Go standard library actually does, not opinions or blog posts. Every pattern cites specific files and line numbers.
|
||||||
|
|
||||||
When unsure how to do something in Go, look at how the standard library does it.
|
When unsure how to do something in Go, look at how the standard library does it.
|
||||||
|
|||||||
@@ -1,325 +0,0 @@
|
|||||||
# Stdlib vs Kubernetes: Where K8s Does Things Differently
|
|
||||||
|
|
||||||
## 1. Concurrency: Channels vs. Condition Variables + Sets
|
|
||||||
|
|
||||||
### Stdlib approach
|
|
||||||
Go stdlib idiom: communicate via channels. A worker pool typically uses a `chan T` for work items.
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Typical stdlib pattern:
|
|
||||||
jobs := make(chan Job, 100)
|
|
||||||
for i := 0; i < workers; i++ {
|
|
||||||
go func() {
|
|
||||||
for job := range jobs {
|
|
||||||
process(job)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Kubernetes approach
|
|
||||||
The workqueue uses `sync.Cond` + `sets.Set` instead of channels.
|
|
||||||
|
|
||||||
**Source:** `staging/src/k8s.io/client-go/util/workqueue/queue.go` (lines 190–300)
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Typed[t comparable] struct {
|
|
||||||
queue Queue[t]
|
|
||||||
dirty sets.Set[t] // Items needing processing
|
|
||||||
processing sets.Set[t] // Items currently being processed
|
|
||||||
cond *sync.Cond // Notification mechanism
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Why K8s is different
|
|
||||||
Channels don't deduplicate. If you send the same key twice to a channel, it gets processed twice. K8s needs:
|
|
||||||
- **Deduplication**: same object modified 5 times → process once with latest state
|
|
||||||
- **Re-entrant marking**: if modified during processing, re-queue after Done()
|
|
||||||
- **Inspection**: can query queue length, processing count for metrics
|
|
||||||
|
|
||||||
A channel-based design would require a separate dedup layer anyway, losing its simplicity advantage.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Error Handling: Single Errors vs. Error Aggregation
|
|
||||||
|
|
||||||
### Stdlib approach
|
|
||||||
Return a single error. Wrap with `fmt.Errorf("...: %w", err)`.
|
|
||||||
|
|
||||||
### Kubernetes approach
|
|
||||||
**Source:** `staging/src/k8s.io/apimachinery/pkg/util/errors/errors.go` (lines 34–100)
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Aggregate interface {
|
|
||||||
error
|
|
||||||
Errors() []error
|
|
||||||
Is(error) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAggregate(errlist []error) Aggregate {
|
|
||||||
// Filters nils, deduplicates messages
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Why K8s is different
|
|
||||||
A controller sync often does multiple operations (create 3 pods, update 2 services). You want to attempt all of them, not fail fast on the first error. Error aggregation collects all failures so the user sees the full picture.
|
|
||||||
|
|
||||||
**Also**: the `Aggregate` interface properly implements `errors.Is()` by checking if *any* contained error matches — which `errors.Join` didn't originally support well.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Retry: stdlib has nothing; K8s has structured retry
|
|
||||||
|
|
||||||
### Stdlib approach
|
|
||||||
There's no retry utility in stdlib. You write your own loop.
|
|
||||||
|
|
||||||
### Kubernetes approach
|
|
||||||
**Source:** `staging/src/k8s.io/client-go/util/retry/util.go` (lines 30–100)
|
|
||||||
|
|
||||||
```go
|
|
||||||
var DefaultRetry = wait.Backoff{
|
|
||||||
Steps: 5,
|
|
||||||
Duration: 10 * time.Millisecond,
|
|
||||||
Factor: 1.0,
|
|
||||||
Jitter: 0.1,
|
|
||||||
}
|
|
||||||
|
|
||||||
func RetryOnConflict(backoff wait.Backoff, fn func() error) error {
|
|
||||||
// Retries only on HTTP 409 Conflict
|
|
||||||
return OnError(backoff, errors.IsConflict, fn)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Why K8s is different
|
|
||||||
In a distributed system, retries are *the* primary reliability mechanism. Stdlib doesn't provide them because stdlib targets single-machine programs. Kubernetes needs:
|
|
||||||
- Configurable backoff (steps, factor, jitter, cap)
|
|
||||||
- Condition-based retry (retry only on specific error types)
|
|
||||||
- Context-aware cancellation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Polling: time.Ticker vs. Contextual Loop With Crash Protection
|
|
||||||
|
|
||||||
### Stdlib approach
|
|
||||||
```go
|
|
||||||
ticker := time.NewTicker(interval)
|
|
||||||
defer ticker.Stop()
|
|
||||||
for range ticker.C {
|
|
||||||
doWork()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Kubernetes approach
|
|
||||||
**Source:** `staging/src/k8s.io/apimachinery/pkg/util/wait/backoff.go` (lines 240–260), `loop.go` (lines 38–80)
|
|
||||||
|
|
||||||
```go
|
|
||||||
func BackoffUntilWithContext(ctx context.Context, f func(ctx context.Context), backoff BackoffManager, sliding bool) {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
if !sliding { t = backoff.Backoff() }
|
|
||||||
func() {
|
|
||||||
defer runtime.HandleCrashWithContext(ctx)
|
|
||||||
f(ctx)
|
|
||||||
}()
|
|
||||||
if sliding { t = backoff.Backoff() }
|
|
||||||
// ... wait for timer or context cancellation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Why K8s is different
|
|
||||||
- **Crash protection**: a panic in `doWork()` shouldn't kill the whole process
|
|
||||||
- **Sliding vs non-sliding**: controls whether interval includes execution time
|
|
||||||
- **Context cancellation**: allows clean shutdown
|
|
||||||
- **Jitter**: prevents thundering herd when many controllers sync at similar intervals
|
|
||||||
- **Double-check for cancellation**: Go's select is non-deterministic, so short timers can "win" against a cancelled context
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Graceful Shutdown: http.Server.Shutdown vs. Multi-Phase Orchestration
|
|
||||||
|
|
||||||
### Stdlib approach (net/http)
|
|
||||||
**Source:** `/tmp/go-src/src/net/http/server.go` (lines 3221–3260)
|
|
||||||
|
|
||||||
```go
|
|
||||||
func (s *Server) Shutdown(ctx context.Context) error {
|
|
||||||
s.inShutdown.Store(true)
|
|
||||||
s.closeListenersLocked()
|
|
||||||
// Poll for idle connections
|
|
||||||
for {
|
|
||||||
if s.closeIdleConns() { return nil }
|
|
||||||
select {
|
|
||||||
case <-ctx.Done(): return ctx.Err()
|
|
||||||
case <-timer.C: timer.Reset(nextPollInterval())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Kubernetes approach
|
|
||||||
**Source:** `pkg/controller/deployment/deployment_controller.go` (lines 171–196)
|
|
||||||
|
|
||||||
```go
|
|
||||||
func (dc *DeploymentController) Run(ctx context.Context, workers int) {
|
|
||||||
defer utilruntime.HandleCrash()
|
|
||||||
dc.eventBroadcaster.StartStructuredLogging(3)
|
|
||||||
dc.eventBroadcaster.StartRecordingToSink(...)
|
|
||||||
defer dc.eventBroadcaster.Shutdown()
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
defer func() {
|
|
||||||
dc.queue.ShutDown() // Stop accepting new work
|
|
||||||
wg.Wait() // Wait for workers to finish
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Gate: don't start until caches are synced
|
|
||||||
if !cache.WaitForNamedCacheSyncWithContext(ctx, ...) { return }
|
|
||||||
|
|
||||||
for i := 0; i < workers; i++ {
|
|
||||||
wg.Go(func() {
|
|
||||||
wait.UntilWithContext(ctx, dc.worker, time.Second)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
<-ctx.Done() // Block until context cancelled
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Why K8s is different
|
|
||||||
Stdlib's shutdown is **reactive** (wait for connections to drain). Kubernetes' shutdown is **multi-phase orchestrated**:
|
|
||||||
1. Stop accepting new events (close watch connections)
|
|
||||||
2. Drain the work queue (process remaining items)
|
|
||||||
3. Wait for in-flight syncs to complete
|
|
||||||
4. Shut down event recorders
|
|
||||||
|
|
||||||
The queue's `ShutDownWithDrain()` is the K8s-specific innovation: it waits until all in-flight items call `Done()`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Type Systems: interfaces vs. Runtime Type Registry
|
|
||||||
|
|
||||||
### Stdlib approach
|
|
||||||
Interfaces for polymorphism. If you need to serialize, use `encoding/json` with struct tags.
|
|
||||||
|
|
||||||
### Kubernetes approach
|
|
||||||
A full runtime type registry (Scheme) that maps between GVK strings and Go types.
|
|
||||||
|
|
||||||
**Source:** `staging/src/k8s.io/apimachinery/pkg/runtime/scheme.go`
|
|
||||||
|
|
||||||
### Why K8s is different
|
|
||||||
Stdlib's `encoding/json` requires knowing the concrete type at compile time. Kubernetes must:
|
|
||||||
- Deserialize objects from the network without knowing their type in advance
|
|
||||||
- Convert between API versions (`v1beta1.Deployment` → `v1.Deployment`)
|
|
||||||
- Support third-party types (CRDs) that don't exist at compile time
|
|
||||||
- Apply defaulting and validation based on type metadata
|
|
||||||
|
|
||||||
This forces a **runtime type system layered on top of Go's static types**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Testing: httptest vs. Fake Clients + Reactors
|
|
||||||
|
|
||||||
### Stdlib approach
|
|
||||||
`net/http/httptest` provides a test server. You make real HTTP calls against it.
|
|
||||||
|
|
||||||
### Kubernetes approach
|
|
||||||
Fake clientsets with reactor chains:
|
|
||||||
```go
|
|
||||||
// Generated fake clients intercept API calls
|
|
||||||
fakeClient := fake.NewSimpleClientset(existingObjects...)
|
|
||||||
fakeClient.PrependReactor("create", "pods", func(action testing.Action) (bool, runtime.Object, error) {
|
|
||||||
// Custom test behavior
|
|
||||||
return true, nil, fmt.Errorf("simulated error")
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Why K8s is different
|
|
||||||
Testing a controller doesn't require a running API server. The fake client + informer pattern lets you:
|
|
||||||
- Inject specific starting states
|
|
||||||
- Simulate failures at specific operations
|
|
||||||
- Run synchronously (no network delay)
|
|
||||||
- Test the controller logic in isolation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Lifecycle: main() returns vs. Infinite Reconciliation
|
|
||||||
|
|
||||||
### Stdlib pattern
|
|
||||||
Programs start, do work, return.
|
|
||||||
|
|
||||||
### Kubernetes pattern
|
|
||||||
Controllers start and run *forever*, continuously reconciling.
|
|
||||||
|
|
||||||
```go
|
|
||||||
// The fundamental difference: stdlib programs terminate, controllers don't
|
|
||||||
func main() {
|
|
||||||
// Stdlib: do work, exit
|
|
||||||
result := compute()
|
|
||||||
fmt.Println(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kubernetes: infinite loop with eventual consistency
|
|
||||||
func (c *Controller) Run(ctx context.Context) {
|
|
||||||
// Run forever until context cancelled
|
|
||||||
for i := 0; i < workers; i++ {
|
|
||||||
go wait.UntilWithContext(ctx, c.worker, time.Second)
|
|
||||||
}
|
|
||||||
<-ctx.Done()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Why K8s is different
|
|
||||||
The real world is adversarial. Networks fail, nodes die, humans make mistakes. A one-shot program can't handle drift. The reconciliation loop is Kubernetes' answer to the CAP theorem: you can't guarantee consistency in a single call, but you can achieve it *eventually* through repetition.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Shared State: Package-Level vs. Shared Informer Cache
|
|
||||||
|
|
||||||
### Stdlib approach
|
|
||||||
Package-level variables, or pass state through function parameters.
|
|
||||||
|
|
||||||
### Kubernetes approach
|
|
||||||
The SharedInformerFactory creates a single in-memory cache per resource type, shared by all controllers in the process.
|
|
||||||
|
|
||||||
```go
|
|
||||||
// All controllers share ONE watch and ONE cache per resource:
|
|
||||||
informerFactory := informers.NewSharedInformerFactory(client, resyncPeriod)
|
|
||||||
deployInformer := informerFactory.Apps().V1().Deployments()
|
|
||||||
|
|
||||||
// Controller A and B both get events from the same informer
|
|
||||||
deployInformer.Informer().AddEventHandler(controllerA)
|
|
||||||
deployInformer.Informer().AddEventHandler(controllerB)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Why K8s is different
|
|
||||||
Without sharing:
|
|
||||||
- 20 controllers × watch for Pods = 20 TCP connections to API server
|
|
||||||
- 20 copies of all Pods in memory
|
|
||||||
|
|
||||||
With SharedInformerFactory:
|
|
||||||
- 1 TCP connection for Pods
|
|
||||||
- 1 copy in memory
|
|
||||||
- Events fanned out to all registered handlers
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Configuration: Flags/Env vs. Feature Gates
|
|
||||||
|
|
||||||
### Stdlib approach
|
|
||||||
`flag` package, environment variables, config files.
|
|
||||||
|
|
||||||
### Kubernetes approach
|
|
||||||
Feature gates: a versioned, lifecycle-aware configuration system.
|
|
||||||
|
|
||||||
### Why K8s is different
|
|
||||||
Stdlib's flag package is for a single binary. Kubernetes has:
|
|
||||||
- Hundreds of features in various stages of maturity
|
|
||||||
- Features that must be consistent across control plane components
|
|
||||||
- Features that need to be enabled/disabled without redeployment
|
|
||||||
- Features with dependencies on other features
|
|
||||||
- Automated testing that exercises all combinations
|
|
||||||
|
|
||||||
Feature gates encode *maturity* (alpha/beta/GA) alongside the boolean value, something `flag.Bool` can never express.
|
|
||||||
@@ -1,522 +0,0 @@
|
|||||||
# Kubernetes-Specific Patterns
|
|
||||||
|
|
||||||
## 1. Controller / Reconciler Pattern
|
|
||||||
|
|
||||||
**Source:** `pkg/controller/deployment/deployment_controller.go` (lines 65–530)
|
|
||||||
|
|
||||||
### What it does
|
|
||||||
The controller pattern is the central design pattern of Kubernetes. Every controller watches a set of resources, maintains a work queue, and reconciles desired state with actual state through a sync loop.
|
|
||||||
|
|
||||||
### Why
|
|
||||||
Distributed systems can't guarantee that a single API call will bring the world to desired state. The controller pattern provides eventual consistency by continuously reconciling — it handles missed events, partial failures, and concurrent modifications.
|
|
||||||
|
|
||||||
### Structure
|
|
||||||
|
|
||||||
```go
|
|
||||||
// pkg/controller/deployment/deployment_controller.go:65-95
|
|
||||||
type DeploymentController struct {
|
|
||||||
rsControl controller.RSControlInterface
|
|
||||||
client clientset.Interface
|
|
||||||
|
|
||||||
// Testability: sync handler is injectable
|
|
||||||
syncHandler func(ctx context.Context, dKey string) error
|
|
||||||
enqueueDeployment func(deployment *apps.Deployment)
|
|
||||||
|
|
||||||
// Listers: read from local cache, not API server
|
|
||||||
dLister appslisters.DeploymentLister
|
|
||||||
rsLister appslisters.ReplicaSetLister
|
|
||||||
podLister corelisters.PodLister
|
|
||||||
|
|
||||||
// Synced funcs: gate processing until caches are warm
|
|
||||||
dListerSynced cache.InformerSynced
|
|
||||||
rsListerSynced cache.InformerSynced
|
|
||||||
|
|
||||||
// Work queue: rate-limited, deduplicating
|
|
||||||
queue workqueue.TypedRateLimitingInterface[string]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### The Canonical Worker Loop
|
|
||||||
|
|
||||||
```go
|
|
||||||
// pkg/controller/deployment/deployment_controller.go:481-515
|
|
||||||
func (dc *DeploymentController) worker(ctx context.Context) {
|
|
||||||
for dc.processNextWorkItem(ctx) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dc *DeploymentController) processNextWorkItem(ctx context.Context) bool {
|
|
||||||
key, quit := dc.queue.Get()
|
|
||||||
if quit {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer dc.queue.Done(key)
|
|
||||||
|
|
||||||
err := dc.syncHandler(ctx, key)
|
|
||||||
dc.handleErr(ctx, err, key)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dc *DeploymentController) handleErr(ctx context.Context, err error, key string) {
|
|
||||||
if err == nil || errors.HasStatusCause(err, v1.NamespaceTerminatingCause) {
|
|
||||||
dc.queue.Forget(key) // Success: clear rate limiter
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if dc.queue.NumRequeues(key) < maxRetries {
|
|
||||||
dc.queue.AddRateLimited(key) // Retry with backoff
|
|
||||||
return
|
|
||||||
}
|
|
||||||
utilruntime.HandleError(err)
|
|
||||||
dc.queue.Forget(key) // Give up after maxRetries
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### When to Use
|
|
||||||
|
|
||||||
**Triggers:**
|
|
||||||
- You're building a system that must maintain desired state over time (not just react to events once)
|
|
||||||
- External state can change outside your control (user edits, crashes, network partitions)
|
|
||||||
- You need automatic recovery from partial failures without human intervention
|
|
||||||
|
|
||||||
**Example — before:**
|
|
||||||
```go
|
|
||||||
// Event-driven: reacts once and hopes nothing changes
|
|
||||||
func handlePodCreated(pod Pod) {
|
|
||||||
assignToNode(pod)
|
|
||||||
// What if the node dies 5 seconds later? Nobody re-assigns.
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example — after:**
|
|
||||||
```go
|
|
||||||
// Controller pattern: continuously reconciles desired vs actual
|
|
||||||
func (c *Scheduler) Reconcile(ctx context.Context, key string) error {
|
|
||||||
pod, err := c.podLister.Get(key)
|
|
||||||
if err != nil { return err }
|
|
||||||
|
|
||||||
if pod.Spec.NodeName == "" {
|
|
||||||
node := c.selectBestNode(pod)
|
|
||||||
return c.assignPodToNode(ctx, pod, node)
|
|
||||||
}
|
|
||||||
// Already assigned — verify node is still healthy
|
|
||||||
if !c.nodeIsReady(pod.Spec.NodeName) {
|
|
||||||
return c.reassignPod(ctx, pod)
|
|
||||||
}
|
|
||||||
return nil // desired state matches actual state
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Properties
|
|
||||||
1. **Level-triggered, not edge-triggered** — the sync loop reads current state, not diffs
|
|
||||||
2. **Idempotent** — running sync twice produces the same result
|
|
||||||
3. **Key-based deduplication** — the workqueue coalesces multiple events for the same object
|
|
||||||
4. **Bounded retries** — exponential backoff with a max retry count (15 retries = ~82s max delay)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Informer + Cache + Workqueue Combo
|
|
||||||
|
|
||||||
**Source:** `staging/src/k8s.io/client-go/tools/cache/shared_informer.go` (lines 144–283), `staging/src/k8s.io/client-go/informers/factory.go` (lines 57–250)
|
|
||||||
|
|
||||||
### What it does
|
|
||||||
The Informer provides a local read cache of API server state, backed by a List+Watch connection. The SharedInformerFactory ensures only one informer per resource type exists per process, preventing duplicate watches.
|
|
||||||
|
|
||||||
### Why
|
|
||||||
- **Reduces API server load**: controllers read from local cache (Listers) instead of hitting the API
|
|
||||||
- **Reduces latency**: events are delivered via callbacks, no polling
|
|
||||||
- **Memory efficiency**: shared informers prevent N controllers from opening N watches
|
|
||||||
|
|
||||||
### Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
API Server
|
|
||||||
│
|
|
||||||
├── List (initial sync)
|
|
||||||
│
|
|
||||||
└── Watch (streaming updates)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
SharedIndexInformer
|
|
||||||
├── local Store (thread-safe cache)
|
|
||||||
├── Indexer (secondary indexes)
|
|
||||||
└── Event Handlers → [Controller1, Controller2, ...]
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
WorkQueue
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
worker goroutines
|
|
||||||
```
|
|
||||||
|
|
||||||
### SharedInformerFactory Pattern
|
|
||||||
|
|
||||||
```go
|
|
||||||
// staging/src/k8s.io/client-go/informers/factory.go:57-77
|
|
||||||
type sharedInformerFactory struct {
|
|
||||||
client kubernetes.Interface
|
|
||||||
lock sync.Mutex
|
|
||||||
informers map[reflect.Type]cache.SharedIndexInformer
|
|
||||||
startedInformers map[reflect.Type]bool
|
|
||||||
wg sync.WaitGroup
|
|
||||||
shuttingDown bool
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Registration via Event Handlers
|
|
||||||
|
|
||||||
```go
|
|
||||||
// pkg/controller/deployment/deployment_controller.go:117-146
|
|
||||||
dInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
|
||||||
AddFunc: func(obj interface{}) { dc.addDeployment(logger, obj) },
|
|
||||||
UpdateFunc: func(oldObj, newObj interface{}) { dc.updateDeployment(logger, oldObj, newObj) },
|
|
||||||
DeleteFunc: func(obj interface{}) { dc.deleteDeployment(logger, obj) },
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Cache Sync Gate
|
|
||||||
|
|
||||||
```go
|
|
||||||
// pkg/controller/deployment/deployment_controller.go:189
|
|
||||||
if !cache.WaitForNamedCacheSyncWithContext(ctx, dc.dListerSynced, dc.rsListerSynced, dc.podListerSynced) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Workqueue: Typed Rate-Limiting Queue
|
|
||||||
|
|
||||||
**Source:** `staging/src/k8s.io/client-go/util/workqueue/queue.go` (lines 33–370), `rate_limiting_queue.go`
|
|
||||||
|
|
||||||
### What it does
|
|
||||||
A concurrent-safe work queue with three critical properties:
|
|
||||||
1. **Deduplication** (dirty set) — same item added twice results in one processing
|
|
||||||
2. **Re-entrancy** (processing set) — if an item is added while being processed, it's re-queued after Done()
|
|
||||||
3. **Rate limiting** — exponential backoff on failures
|
|
||||||
|
|
||||||
### Why
|
|
||||||
In a controller, multiple events may fire for the same object in rapid succession. Without deduplication, you'd process stale intermediate states. The dirty/processing set design ensures you always process the latest state while never losing notifications.
|
|
||||||
|
|
||||||
### When to Use
|
|
||||||
|
|
||||||
**Triggers:**
|
|
||||||
- Multiple event sources (informer callbacks) trigger work on the same object rapidly
|
|
||||||
- You need to deduplicate: 5 events for the same pod should result in 1 sync, not 5
|
|
||||||
- Failed processing should retry with exponential backoff, not flood the system
|
|
||||||
|
|
||||||
**Example — before:**
|
|
||||||
```go
|
|
||||||
// Raw channel: no deduplication, no backoff
|
|
||||||
events := make(chan string, 100)
|
|
||||||
|
|
||||||
// Producer fires rapid updates:
|
|
||||||
events <- "pod-abc" // event 1
|
|
||||||
events <- "pod-abc" // event 2 (duplicate!)
|
|
||||||
events <- "pod-abc" // event 3 (duplicate!)
|
|
||||||
|
|
||||||
// Consumer processes all 3 — wasteful
|
|
||||||
for key := range events {
|
|
||||||
reconcile(key) // called 3 times for the same stale state
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example — after:**
|
|
||||||
```go
|
|
||||||
queue := workqueue.NewTypedRateLimitingQueue[string](
|
|
||||||
workqueue.DefaultTypedControllerRateLimiter[string](),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Producer fires rapid updates — queue deduplicates:
|
|
||||||
queue.Add("pod-abc") // queued
|
|
||||||
queue.Add("pod-abc") // already dirty — no-op
|
|
||||||
queue.Add("pod-abc") // already dirty — no-op
|
|
||||||
|
|
||||||
// Consumer processes once with latest state:
|
|
||||||
key, _ := queue.Get()
|
|
||||||
err := reconcile(key) // called once
|
|
||||||
if err != nil {
|
|
||||||
queue.AddRateLimited(key) // retry with backoff
|
|
||||||
} else {
|
|
||||||
queue.Forget(key) // clear backoff counter
|
|
||||||
}
|
|
||||||
queue.Done(key)
|
|
||||||
```
|
|
||||||
|
|
||||||
### The Dirty/Processing Dance
|
|
||||||
|
|
||||||
```go
|
|
||||||
// staging/src/k8s.io/client-go/util/workqueue/queue.go:227-252
|
|
||||||
func (q *Typed[T]) Add(item T) {
|
|
||||||
q.cond.L.Lock()
|
|
||||||
defer q.cond.L.Unlock()
|
|
||||||
if q.shuttingDown { return }
|
|
||||||
if q.dirty.Has(item) {
|
|
||||||
if !q.processing.Has(item) {
|
|
||||||
q.queue.Touch(item) // Allow priority changes
|
|
||||||
}
|
|
||||||
return // Already marked for processing
|
|
||||||
}
|
|
||||||
q.dirty.Insert(item)
|
|
||||||
if q.processing.Has(item) {
|
|
||||||
return // Being processed, will re-queue on Done()
|
|
||||||
}
|
|
||||||
q.queue.Push(item)
|
|
||||||
q.cond.Signal()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Typed[T]) Done(item T) {
|
|
||||||
q.cond.L.Lock()
|
|
||||||
defer q.cond.L.Unlock()
|
|
||||||
q.processing.Delete(item)
|
|
||||||
if q.dirty.Has(item) {
|
|
||||||
q.queue.Push(item) // Was modified during processing
|
|
||||||
q.cond.Signal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rate-Limited Requeue
|
|
||||||
|
|
||||||
```go
|
|
||||||
// staging/src/k8s.io/client-go/util/workqueue/rate_limiting_queue.go:120-122
|
|
||||||
func (q *rateLimitingType[T]) AddRateLimited(item T) {
|
|
||||||
q.TypedDelayingInterface.AddAfter(item, q.rateLimiter.When(item))
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Tombstone Pattern (DeletedFinalStateUnknown)
|
|
||||||
|
|
||||||
**Source:** `staging/src/k8s.io/client-go/tools/cache/delta_fifo.go` (lines 797–801)
|
|
||||||
|
|
||||||
### What it does
|
|
||||||
When a watch disconnects and reconnects, some delete events may be missed. The DeltaFIFO synthesizes a `DeletedFinalStateUnknown` ("tombstone") containing the last known state of the object.
|
|
||||||
|
|
||||||
### Why
|
|
||||||
Without this, controllers would never learn about deletions that happened during disconnects.
|
|
||||||
|
|
||||||
```go
|
|
||||||
// staging/src/k8s.io/client-go/tools/cache/delta_fifo.go:797-801
|
|
||||||
type DeletedFinalStateUnknown struct {
|
|
||||||
Key string
|
|
||||||
Obj interface{}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### How controllers handle it
|
|
||||||
|
|
||||||
```go
|
|
||||||
// pkg/controller/deployment/deployment_controller.go:210-224
|
|
||||||
func (dc *DeploymentController) deleteDeployment(logger klog.Logger, obj interface{}) {
|
|
||||||
d, ok := obj.(*apps.Deployment)
|
|
||||||
if !ok {
|
|
||||||
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
|
|
||||||
if !ok {
|
|
||||||
utilruntime.HandleError(fmt.Errorf("couldn't get object from tombstone %#v", obj))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
d, ok = tombstone.Obj.(*apps.Deployment)
|
|
||||||
if !ok {
|
|
||||||
utilruntime.HandleError(fmt.Errorf("tombstone contained object that is not a Deployment %#v", obj))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dc.enqueueDeployment(d)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Controller Expectations Pattern
|
|
||||||
|
|
||||||
**Source:** `pkg/controller/controller_utils.go` (lines 130–315)
|
|
||||||
|
|
||||||
### What it does
|
|
||||||
Expectations track pending creates/deletes to prevent controllers from taking action on stale cache state. A controller won't sync until its expectations are satisfied or expired.
|
|
||||||
|
|
||||||
### Why
|
|
||||||
Between a controller issuing a create and the informer cache reflecting that new object, there's a window where the controller might create duplicates. Expectations close this gap.
|
|
||||||
|
|
||||||
```go
|
|
||||||
// pkg/controller/controller_utils.go:153-173
|
|
||||||
type ControllerExpectationsInterface interface {
|
|
||||||
GetExpectations(controllerKey string) (*ControlleeExpectations, bool, error)
|
|
||||||
SatisfiedExpectations(logger klog.Logger, controllerKey string) bool
|
|
||||||
DeleteExpectations(logger klog.Logger, controllerKey string)
|
|
||||||
SetExpectations(logger klog.Logger, controllerKey string, add, del int) error
|
|
||||||
ExpectCreations(logger klog.Logger, controllerKey string, adds int) error
|
|
||||||
ExpectDeletions(logger klog.Logger, controllerKey string, dels int) error
|
|
||||||
CreationObserved(logger klog.Logger, controllerKey string)
|
|
||||||
DeletionObserved(logger klog.Logger, controllerKey string)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. OwnerReference / Controller Ref Manager Pattern
|
|
||||||
|
|
||||||
**Source:** `pkg/controller/controller_ref_manager.go` (lines 37–80)
|
|
||||||
|
|
||||||
### What it does
|
|
||||||
Implements garbage collection ownership through OwnerReferences. The ControllerRefManager handles adopting orphaned resources and releasing resources that no longer match.
|
|
||||||
|
|
||||||
### Why
|
|
||||||
Multiple controllers may create children. The ownership model ensures exactly one controller owns each child, enabling garbage collection and preventing conflicts.
|
|
||||||
|
|
||||||
```go
|
|
||||||
// pkg/controller/controller_ref_manager.go:37-50
|
|
||||||
type BaseControllerRefManager struct {
|
|
||||||
Controller metav1.Object
|
|
||||||
Selector labels.Selector
|
|
||||||
canAdoptErr error
|
|
||||||
canAdoptOnce sync.Once // Lazy, one-shot adoption check
|
|
||||||
CanAdoptFunc func(ctx context.Context) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// The claim logic: adopt if matching and orphaned, release if owned but not matching
|
|
||||||
func (m *BaseControllerRefManager) ClaimObject(ctx context.Context, obj metav1.Object,
|
|
||||||
match func(metav1.Object) bool,
|
|
||||||
adopt, release func(context.Context, metav1.Object) error) (bool, error) {
|
|
||||||
controllerRef := metav1.GetControllerOfNoCopy(obj)
|
|
||||||
if controllerRef != nil {
|
|
||||||
if controllerRef.UID != m.Controller.GetUID() {
|
|
||||||
return false, nil // Owned by someone else
|
|
||||||
}
|
|
||||||
if match(obj) {
|
|
||||||
return true, nil // Already ours and matches
|
|
||||||
}
|
|
||||||
// Ours but no longer matches → release
|
|
||||||
}
|
|
||||||
// Orphan → adopt if matches
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Leader Election Pattern
|
|
||||||
|
|
||||||
**Source:** `staging/src/k8s.io/client-go/tools/leaderelection/leaderelection.go` (lines 116–230)
|
|
||||||
|
|
||||||
### What it does
|
|
||||||
Provides distributed mutex semantics using a Kubernetes resource (Lease) as the lock. Only one instance of a controller runs actively; others are hot standbys.
|
|
||||||
|
|
||||||
### When to Use
|
|
||||||
|
|
||||||
**Triggers:**
|
|
||||||
- You're running multiple replicas of a controller for high availability
|
|
||||||
- Only ONE instance should actively reconcile at a time (to avoid conflicts)
|
|
||||||
- You need automatic failover: if the leader dies, another replica takes over within seconds
|
|
||||||
|
|
||||||
**Example — before:**
|
|
||||||
```go
|
|
||||||
// All replicas reconcile simultaneously → write conflicts, duplicate work
|
|
||||||
func main() {
|
|
||||||
ctrl := NewController()
|
|
||||||
ctrl.Run(ctx) // every replica does this — chaos
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example — after:**
|
|
||||||
```go
|
|
||||||
func main() {
|
|
||||||
ctrl := NewController()
|
|
||||||
leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{
|
|
||||||
Lock: resourceLock,
|
|
||||||
LeaseDuration: 15 * time.Second,
|
|
||||||
RenewDeadline: 10 * time.Second,
|
|
||||||
RetryPeriod: 2 * time.Second,
|
|
||||||
Callbacks: leaderelection.LeaderCallbacks{
|
|
||||||
OnStartedLeading: func(ctx context.Context) {
|
|
||||||
ctrl.Run(ctx) // only the leader reconciles
|
|
||||||
},
|
|
||||||
OnStoppedLeading: func() {
|
|
||||||
log.Fatal("lost leadership") // restart to re-enter election
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Why
|
|
||||||
Controller-manager runs multiple replicas for HA. Only one should reconcile to avoid conflicts.
|
|
||||||
|
|
||||||
```go
|
|
||||||
// staging/src/k8s.io/client-go/tools/leaderelection/leaderelection.go:116-163
|
|
||||||
type LeaderElectionConfig struct {
|
|
||||||
Lock rl.Interface
|
|
||||||
LeaseDuration time.Duration // Default 15s — how long a lease is valid
|
|
||||||
RenewDeadline time.Duration // Default 10s — how long leader retries renewal
|
|
||||||
RetryPeriod time.Duration // Default 2s — how often candidates check
|
|
||||||
Callbacks LeaderCallbacks
|
|
||||||
ReleaseOnCancel bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type LeaderCallbacks struct {
|
|
||||||
OnStartedLeading func(context.Context)
|
|
||||||
OnStoppedLeading func()
|
|
||||||
OnNewLeader func(identity string)
|
|
||||||
}
|
|
||||||
|
|
||||||
// staging/src/k8s.io/client-go/tools/leaderelection/leaderelection.go:211-226
|
|
||||||
func (le *LeaderElector) Run(ctx context.Context) {
|
|
||||||
defer runtime.HandleCrashWithContext(ctx)
|
|
||||||
defer le.config.Callbacks.OnStoppedLeading()
|
|
||||||
if !le.acquire(ctx) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
|
||||||
defer cancel()
|
|
||||||
go le.config.Callbacks.OnStartedLeading(ctx)
|
|
||||||
le.renew(ctx)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Feature Gates Pattern
|
|
||||||
|
|
||||||
**Source:** `pkg/features/kube_features.go` (lines 34–2811), `staging/src/k8s.io/client-go/features/features.go` (lines 34–80)
|
|
||||||
|
|
||||||
### What it does
|
|
||||||
A global registry of boolean flags that control feature rollout. Features progress through Alpha → Beta → GA → Deprecated lifecycle stages.
|
|
||||||
|
|
||||||
### Why
|
|
||||||
Kubernetes serves thousands of clusters. Features must be safe to enable/disable at runtime across versions. Feature gates provide:
|
|
||||||
- Progressive rollout (alpha off by default, beta on, GA locked)
|
|
||||||
- Per-version semantics (a feature may become beta in v1.28)
|
|
||||||
- Testing isolation
|
|
||||||
|
|
||||||
```go
|
|
||||||
// staging/src/k8s.io/client-go/features/features.go:50-70
|
|
||||||
type Feature string
|
|
||||||
|
|
||||||
type FeatureSpec struct {
|
|
||||||
Default bool
|
|
||||||
LockToDefault bool
|
|
||||||
PreRelease prerelease
|
|
||||||
Version *version.Version
|
|
||||||
}
|
|
||||||
|
|
||||||
type Gates interface {
|
|
||||||
Enabled(key Feature) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// pkg/features/kube_features.go:50-58 (example feature definition)
|
|
||||||
const (
|
|
||||||
AllowDNSOnlyNodeCSR featuregate.Feature = "AllowDNSOnlyNodeCSR"
|
|
||||||
// ... 2700+ lines of feature definitions
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Registration at init()
|
|
||||||
|
|
||||||
```go
|
|
||||||
// pkg/features/kube_features.go:2798-2811
|
|
||||||
func init() {
|
|
||||||
ca := &clientAdapter{utilfeature.DefaultMutableFeatureGate}
|
|
||||||
runtime.Must(clientfeatures.AddVersionedFeaturesToExistingFeatureGates(ca))
|
|
||||||
clientfeatures.ReplaceFeatureGates(ca)
|
|
||||||
runtime.Must(utilfeature.DefaultMutableFeatureGate.AddVersioned(defaultVersionedKubernetesFeatureGates))
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,441 +0,0 @@
|
|||||||
# Production Go Patterns (from Kubernetes)
|
|
||||||
|
|
||||||
Patterns for building large-scale Go codebases that go beyond what stdlib teaches you.
|
|
||||||
|
|
||||||
## 1. Code Generation Pattern
|
|
||||||
|
|
||||||
**Source:** `staging/src/k8s.io/apimachinery/pkg/runtime/zz_generated.deepcopy.go`, `staging/src/k8s.io/client-go/informers/apps/v1/deployment.go`
|
|
||||||
|
|
||||||
### What it does
|
|
||||||
Kubernetes generates massive amounts of boilerplate code from annotations on types:
|
|
||||||
- `deepcopy-gen` → DeepCopy/DeepCopyInto methods
|
|
||||||
- `informer-gen` → typed informers (List/Watch/Lister per resource)
|
|
||||||
- `client-gen` → typed client sets
|
|
||||||
- `lister-gen` → typed lister interfaces
|
|
||||||
- `conversion-gen` → version conversion functions
|
|
||||||
- `defaulter-gen` → defaulting functions
|
|
||||||
|
|
||||||
### Why
|
|
||||||
At Kubernetes scale (~50 resource types × multiple versions), hand-writing deep copy, client wrappers, and conversion code is:
|
|
||||||
1. Error-prone (forgetting to copy a new field breaks everything)
|
|
||||||
2. Unmaintainable (thousands of nearly-identical files)
|
|
||||||
3. Not verifiable by human review
|
|
||||||
|
|
||||||
### How it works
|
|
||||||
|
|
||||||
Annotations drive generation:
|
|
||||||
```go
|
|
||||||
// +k8s:deepcopy-gen=true
|
|
||||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
|
||||||
type RawExtension struct { ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
Generated output uses `zz_generated.` prefix (convention for "don't edit"):
|
|
||||||
```go
|
|
||||||
// staging/src/k8s.io/apimachinery/pkg/runtime/zz_generated.deepcopy.go:22
|
|
||||||
// Code generated by deepcopy-gen. DO NOT EDIT.
|
|
||||||
package runtime
|
|
||||||
|
|
||||||
func (in *RawExtension) DeepCopyInto(out *RawExtension) {
|
|
||||||
*out = *in
|
|
||||||
if in.Raw != nil {
|
|
||||||
in, out := &in.Raw, &out.Raw
|
|
||||||
*out = make([]byte, len(*in))
|
|
||||||
copy(*out, *in)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Generated informers (note the header comment):
|
|
||||||
```go
|
|
||||||
// staging/src/k8s.io/client-go/informers/apps/v1/deployment.go:20
|
|
||||||
// Code generated by informer-gen. DO NOT EDIT.
|
|
||||||
```
|
|
||||||
|
|
||||||
### When to Use
|
|
||||||
|
|
||||||
**Triggers:**
|
|
||||||
- You have 10+ types that need identical boilerplate methods (DeepCopy, Validate, Marshal)
|
|
||||||
- Hand-writing the code is error-prone (forgetting to copy a new field causes silent bugs)
|
|
||||||
- The generated output is mechanical and reviewable, not creative
|
|
||||||
|
|
||||||
**Example — before:**
|
|
||||||
```go
|
|
||||||
// Hand-written deep copy for every type — 50 types × 30 lines each = 1500 lines of bugs
|
|
||||||
func (in *Deployment) DeepCopy() *Deployment {
|
|
||||||
out := new(Deployment)
|
|
||||||
out.Name = in.Name
|
|
||||||
out.Labels = make(map[string]string)
|
|
||||||
for k, v := range in.Labels { out.Labels[k] = v }
|
|
||||||
// Did you remember Annotations? Finalizers? Every nested struct?
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example — after:**
|
|
||||||
```go
|
|
||||||
// +k8s:deepcopy-gen=true
|
|
||||||
type Deployment struct {
|
|
||||||
Name string
|
|
||||||
Labels map[string]string
|
|
||||||
Annotations map[string]string
|
|
||||||
}
|
|
||||||
// Generated: zz_generated.deepcopy.go handles ALL fields correctly, always.
|
|
||||||
// Adding a new field? Re-run generator. Zero chance of forgetting.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Insight
|
|
||||||
**Stdlib has no code generation culture.** stdlib keeps things small enough that hand-writing works. Kubernetes proves that once you cross ~20 types with shared behavior, code gen is the only sane path.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. The Scheme / Type Registry Pattern
|
|
||||||
|
|
||||||
**Source:** `staging/src/k8s.io/apimachinery/pkg/runtime/scheme.go` (lines 38–100), `scheme_builder.go`
|
|
||||||
|
|
||||||
### What it does
|
|
||||||
The Scheme is a runtime type registry that maps:
|
|
||||||
- `GroupVersionKind` → Go type (`reflect.Type`)
|
|
||||||
- Go type → `[]GroupVersionKind`
|
|
||||||
- Provides serialization, defaulting, conversion, and validation dispatch
|
|
||||||
|
|
||||||
### Why
|
|
||||||
Kubernetes has 50+ resource types across 15+ API groups, each with multiple versions. The Scheme provides:
|
|
||||||
- **Dynamic dispatch**: serialize any Object without knowing its concrete type
|
|
||||||
- **Version conversion**: convert between v1 and v1beta1 transparently
|
|
||||||
- **Pluggability**: third-party resources register into the same system
|
|
||||||
|
|
||||||
### Structure
|
|
||||||
|
|
||||||
```go
|
|
||||||
// staging/src/k8s.io/apimachinery/pkg/runtime/scheme.go:38-98
|
|
||||||
type Scheme struct {
|
|
||||||
gvkToType map[schema.GroupVersionKind]reflect.Type
|
|
||||||
typeToGVK map[reflect.Type][]schema.GroupVersionKind
|
|
||||||
unversionedTypes map[reflect.Type]schema.GroupVersionKind
|
|
||||||
defaulterFuncs map[reflect.Type]func(interface{})
|
|
||||||
validationFuncs map[reflect.Type]func(ctx, op, obj, oldObj) field.ErrorList
|
|
||||||
converter *conversion.Converter
|
|
||||||
versionPriority map[string][]string
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### SchemeBuilder Pattern
|
|
||||||
|
|
||||||
```go
|
|
||||||
// staging/src/k8s.io/apimachinery/pkg/runtime/scheme_builder.go:23-48
|
|
||||||
type SchemeBuilder []func(*Scheme) error
|
|
||||||
|
|
||||||
func (sb *SchemeBuilder) AddToScheme(s *Scheme) error {
|
|
||||||
for _, f := range *sb {
|
|
||||||
if err := f(s); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sb *SchemeBuilder) Register(funcs ...func(*Scheme) error) {
|
|
||||||
*sb = append(*sb, f)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### How Registration Works
|
|
||||||
|
|
||||||
```go
|
|
||||||
// staging/src/k8s.io/apimachinery/pkg/runtime/scheme.go:151-160
|
|
||||||
func (s *Scheme) AddKnownTypes(gv schema.GroupVersion, types ...Object) {
|
|
||||||
for _, obj := range types {
|
|
||||||
t := reflect.TypeOf(obj)
|
|
||||||
if t.Kind() != reflect.Pointer {
|
|
||||||
panic("All types must be pointers to structs.")
|
|
||||||
}
|
|
||||||
t = t.Elem()
|
|
||||||
s.AddKnownTypeWithName(gv.WithKind(t.Name()), obj)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Insight
|
|
||||||
This is Java's ServiceLoader / dependency injection adapted for Go's type system. Stdlib uses interfaces; Kubernetes needs a **runtime type system on top of Go's static type system** because API objects must be dynamically dispatched across version boundaries.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. The runtime.Object Interface
|
|
||||||
|
|
||||||
**Source:** `staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go` (lines 333–342)
|
|
||||||
|
|
||||||
### What it does
|
|
||||||
Every Kubernetes API object must implement this two-method interface:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go:337-341
|
|
||||||
type Object interface {
|
|
||||||
GetObjectKind() schema.ObjectKind
|
|
||||||
DeepCopyObject() Object
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Why
|
|
||||||
- `GetObjectKind()` — allows the serialization layer to determine what type an object is without reflection
|
|
||||||
- `DeepCopyObject()` — enables safe concurrent access (informer cache is shared; mutations must happen on copies)
|
|
||||||
|
|
||||||
### Key Insight
|
|
||||||
**This is the foundation of Kubernetes' extensibility.** Any Go struct that satisfies these two methods can participate in the entire API machinery — serialization, storage, admission, informers, etc. CRDs generate code that implements this interface.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Deep Copy Everywhere
|
|
||||||
|
|
||||||
**Source:** Generated code in `zz_generated.deepcopy.go` files throughout the tree
|
|
||||||
|
|
||||||
### What it does
|
|
||||||
Every API type has generated `DeepCopy()` and `DeepCopyInto()` methods that create true deep copies including nested slices, maps, and pointer fields.
|
|
||||||
|
|
||||||
### Why
|
|
||||||
The informer cache is shared across all controllers in a process. If controller A gets an object from the cache and mutates it, controller B would see corrupted data. Deep copy provides the isolation guarantee.
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Usage pattern in controllers:
|
|
||||||
deployment := deploymentFromCache.DeepCopy()
|
|
||||||
deployment.Spec.Replicas = ptr.To[int32](3)
|
|
||||||
_, err := client.AppsV1().Deployments(ns).Update(ctx, deployment, metav1.UpdateOptions{})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Insight
|
|
||||||
Stdlib rarely needs deep copy because stdlib objects are typically owned by one goroutine. Kubernetes has a **shared read cache** (the informer store) that necessitates copy-on-write semantics at the application level.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Graceful Shutdown with Priority Classes
|
|
||||||
|
|
||||||
**Source:** `pkg/kubelet/nodeshutdown/nodeshutdown_manager_linux.go` (lines 23–100)
|
|
||||||
|
|
||||||
### What it does
|
|
||||||
When a node is shutting down, pods are terminated in priority order. Critical pods (system-node-critical) get more grace time than regular pods.
|
|
||||||
|
|
||||||
### Why
|
|
||||||
A hard kill of all pods simultaneously would lose important work. Priority-based graceful shutdown preserves the most important workloads longest.
|
|
||||||
|
|
||||||
```go
|
|
||||||
// pkg/kubelet/nodeshutdown/nodeshutdown_manager_linux.go:66-90
|
|
||||||
type managerImpl struct {
|
|
||||||
logger klog.Logger
|
|
||||||
recorder record.EventRecorder
|
|
||||||
getPods eviction.ActivePodsFunc
|
|
||||||
syncNodeStatus func(context.Context)
|
|
||||||
dbusCon dbusInhibiter
|
|
||||||
inhibitLock systemd.InhibitLock
|
|
||||||
nodeShuttingDownMutex sync.Mutex
|
|
||||||
nodeShuttingDownNow bool
|
|
||||||
podManager *podManager
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Context as Logger Carrier
|
|
||||||
|
|
||||||
**Source:** `pkg/controller/deployment/deployment_controller.go` (lines 106, 179, 500)
|
|
||||||
|
|
||||||
### What it does
|
|
||||||
Kubernetes passes structured loggers through context:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// pkg/controller/deployment/deployment_controller.go:179
|
|
||||||
logger := klog.FromContext(ctx)
|
|
||||||
logger.Info("Starting controller", "controller", "deployment")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Why
|
|
||||||
At scale, you need structured logging with:
|
|
||||||
- Consistent key-value pairs (controller name, object reference)
|
|
||||||
- Verbosity levels (`logger.V(4).Info(...)`)
|
|
||||||
- No global state (context carries the logger configured by the caller)
|
|
||||||
|
|
||||||
### Key Insight
|
|
||||||
Stdlib's `log` package is global. Kubernetes uses context-based structured logging (`klog.FromContext`) to allow each call chain to carry its own logger configuration. This enables filtering by controller, verbosity tuning per-component, and correlation.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Functional Options for Configuration
|
|
||||||
|
|
||||||
**Source:** `staging/src/k8s.io/client-go/informers/factory.go` (lines 83–127)
|
|
||||||
|
|
||||||
### What it does
|
|
||||||
The SharedInformerFactory uses functional options for configuration:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// staging/src/k8s.io/client-go/informers/factory.go:57
|
|
||||||
type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory
|
|
||||||
|
|
||||||
func WithNamespace(namespace string) SharedInformerOption {
|
|
||||||
return func(factory *sharedInformerFactory) *sharedInformerFactory {
|
|
||||||
factory.namespace = namespace
|
|
||||||
return factory
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithTransform(transform cache.TransformFunc) SharedInformerOption {
|
|
||||||
return func(factory *sharedInformerFactory) *sharedInformerFactory {
|
|
||||||
factory.transform = transform
|
|
||||||
return factory
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSharedInformerFactoryWithOptions(client kubernetes.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory {
|
|
||||||
factory := &sharedInformerFactory{...}
|
|
||||||
for _, opt := range options {
|
|
||||||
factory = opt(factory)
|
|
||||||
}
|
|
||||||
return factory
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Why
|
|
||||||
APIs evolve. Adding a new configuration option shouldn't break callers. Functional options provide:
|
|
||||||
- Backward compatibility (new options don't change existing signatures)
|
|
||||||
- Self-documenting (each option is a named function)
|
|
||||||
- Composability (options can be collected and applied conditionally)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Type-Safe Generics in Critical Paths
|
|
||||||
|
|
||||||
**Source:** `staging/src/k8s.io/client-go/util/workqueue/queue.go` (lines 33–200), `staging/src/k8s.io/client-go/gentype/type.go` (lines 33–120)
|
|
||||||
|
|
||||||
### What it does
|
|
||||||
Both workqueue and gentype use Go generics (1.18+) to provide type-safe interfaces while maintaining backward compatibility via type aliases:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Workqueue: type-safe queue
|
|
||||||
type TypedInterface[T comparable] interface {
|
|
||||||
Add(item T)
|
|
||||||
Get() (item T, shutdown bool)
|
|
||||||
Done(item T)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type alias for backward compat
|
|
||||||
type Type = Typed[any]
|
|
||||||
|
|
||||||
// Gentype: type-safe client
|
|
||||||
type Client[T objectWithMeta] struct {
|
|
||||||
resource string
|
|
||||||
client rest.Interface
|
|
||||||
namespace string
|
|
||||||
newObject func() T
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Why
|
|
||||||
Before generics, Kubernetes used `interface{}` everywhere, requiring type assertions at every boundary. Generics eliminate entire classes of runtime panics and make the code self-documenting.
|
|
||||||
|
|
||||||
### Key Insight
|
|
||||||
This is a migration pattern: introduce the generic version alongside the deprecated `interface{}` version using type aliases. Callers migrate at their own pace.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. HandleCrash — Structured Panic Recovery
|
|
||||||
|
|
||||||
**Source:** `staging/src/k8s.io/apimachinery/pkg/util/runtime/runtime.go` (lines 30–120)
|
|
||||||
|
|
||||||
### What it does
|
|
||||||
A standardized `defer HandleCrash()` pattern that:
|
|
||||||
1. Catches panics
|
|
||||||
2. Logs them with proper stack attribution
|
|
||||||
3. Invokes registered panic handlers
|
|
||||||
4. Optionally re-panics (controlled by `ReallyCrash` flag)
|
|
||||||
|
|
||||||
```go
|
|
||||||
// staging/src/k8s.io/apimachinery/pkg/util/runtime/runtime.go:78-82
|
|
||||||
func HandleCrashWithContext(ctx context.Context, additionalHandlers ...func(context.Context, interface{})) {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
handleCrash(ctx, r, additionalHandlers...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Why
|
|
||||||
In a production system with hundreds of goroutines, an unrecovered panic in one kills the entire process. HandleCrash provides a standardized recovery point that:
|
|
||||||
- Logs the panic with caller attribution
|
|
||||||
- Allows cleanup handlers (shutdown gracefully)
|
|
||||||
- In tests, can be configured to not actually crash
|
|
||||||
|
|
||||||
### When to Use
|
|
||||||
|
|
||||||
**Triggers:**
|
|
||||||
- You're running multiple independent subsystems in one process (multiple controllers, background workers)
|
|
||||||
- A panic in one subsystem shouldn't kill the entire process
|
|
||||||
- You need structured logging of panic stack traces before potential recovery
|
|
||||||
|
|
||||||
**Example — before:**
|
|
||||||
```go
|
|
||||||
// One bad nil pointer in workerB kills workerA, workerC, and the whole server
|
|
||||||
func main() {
|
|
||||||
go workerA(ctx)
|
|
||||||
go workerB(ctx) // panics → entire process dies
|
|
||||||
go workerC(ctx)
|
|
||||||
select {}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example — after:**
|
|
||||||
```go
|
|
||||||
func safeGo(ctx context.Context, name string, f func(ctx context.Context)) {
|
|
||||||
go func() {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
log.Printf("panic in %s: %v
|
|
||||||
%s", name, r, debug.Stack())
|
|
||||||
// Log, alert, increment metric — but don't kill siblings
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
f(ctx)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
safeGo(ctx, "worker-a", workerA)
|
|
||||||
safeGo(ctx, "worker-b", workerB) // panics → logged, other workers continue
|
|
||||||
safeGo(ctx, "worker-c", workerC)
|
|
||||||
select {}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Insight
|
|
||||||
Stdlib's approach is "let it crash." Kubernetes' approach is "catch it, log it, let the controller retry on the next sync." This is only safe because the controller pattern is idempotent.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. ContextForChannel — Bridge Pattern
|
|
||||||
|
|
||||||
**Source:** `staging/src/k8s.io/apimachinery/pkg/util/wait/wait.go` (lines 120–145)
|
|
||||||
|
|
||||||
### What it does
|
|
||||||
Bridges the older `<-chan struct{}` stop pattern to the modern `context.Context` pattern:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// staging/src/k8s.io/apimachinery/pkg/util/wait/wait.go:120-142
|
|
||||||
func ContextForChannel(parentCh <-chan struct{}) context.Context {
|
|
||||||
return channelContext{stopCh: parentCh}
|
|
||||||
}
|
|
||||||
|
|
||||||
type channelContext struct {
|
|
||||||
stopCh <-chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c channelContext) Done() <-chan struct{} { return c.stopCh }
|
|
||||||
func (c channelContext) Err() error {
|
|
||||||
select {
|
|
||||||
case <-c.stopCh:
|
|
||||||
return context.Canceled
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Why
|
|
||||||
Kubernetes predates `context.Context` (which arrived in Go 1.7). Millions of lines of code use `stopCh <-chan struct{}`. Rather than a big-bang rewrite, this adapter allows gradual migration.
|
|
||||||
|
|
||||||
### Key Insight
|
|
||||||
**Large codebases can't do breaking API changes atomically.** This bridge pattern is how you evolve from one idiom to another over years without breaking everything at once.
|
|
||||||
Reference in New Issue
Block a user