Tekton Pipelines — Patterns#

Concurrency patterns#

Level-triggered Work Queue (inherited from Knative)#

  • Usage: Core reconciliation model for all three controllers (TaskRun, PipelineRun, ResolutionRequest)
  • Example: pkg/client/injection/reconciler/pipeline/v1/taskrun/controller.gocontroller.NewContext(ctx, rec, controller.ControllerOptions{...}) creates a work queue backed reconciler; Kubernetes informer events enqueue items; configurable thread count (THREADS_PER_CONTROLLER)
  • Assessment: Highly idiomatic for Kubernetes operators. The queue absorbs bursts and ensures at-least-once reconciliation. Tekton inherits this wholesale from Knative — no custom implementation.

Fan-out with errgroup + channel drain#

  • Usage: Result file reading in internal/sidecarlogresults — reads N result files concurrently
  • Example: internal/sidecarlogresults/sidecarlogresults.go:158-205 — launches a goroutine per result file via errgroup.Group, sends SidecarLogResult values to an unbuffered results channel; a second channelGroup goroutine closes the channel after all workers finish; the main goroutine range-drains the channel
  • Assessment: Canonical fan-out/fan-in with errgroup. Uses a two-group pattern (workers + closer) to coordinate channel closure cleanly. Effective and idiomatic.

errgroup for parallel resolution#

  • Usage: pkg/reconciler/taskrun/resources/taskspec.go:222 — parallel resolution of task specs
  • Example: errgroup.WithContext(ctx) used with goroutines; context cancellation propagates on first error
  • Assessment: Correct use for fork-join with error collection.

Context cancellation as step lifecycle gate#

  • Usage: cmd/entrypoint/waiter.go:77 — the Entrypointer waiter polls for step-start semaphore files; ctx.Done() via select provides timeout/cancellation
  • Example: waiter.go: case <-ctx.Done(): return ctx.Err() — uses select to interleave polling with cancellation
  • Assessment: Straightforward and correct. Context carries both timeout and cancellation signals.

Graceful shutdown via signal context#

  • Usage: All binary entry points (cmd/controller, cmd/resolvers, cmd/webhook, cmd/sidecarlogresults)
  • Example: cmd/controller/main.go:86ctx := injection.WithNamespaceScope(signals.NewContext(), *namespace) — Knative’s signals.NewContext() returns a context cancelled on SIGTERM/SIGINT
  • Assessment: Clean pattern. Binary-level shutdown is fully delegated to Knative’s signal handling; no manual os.Signal channels in controller code (though cmd/sidecarlogresults does it manually due to its simpler structure).

Goroutine count#

  • Raw usage: 61 go func occurrences (excluding vendor) — moderate for an operator-style project. Most goroutines are managed by the Knative work queue, not spawned ad-hoc.

Select statements#

  • Count: 33 non-vendor occurrences — used primarily in entrypoint polling loops and context-aware waits.

Sync primitives#

  • Count: 34 non-vendor occurrences of sync.Mutex, sync.RWMutex, sync.Once, sync.WaitGroup, sync.Map, or atomic.* — low, reflecting that shared state is mostly in Kubernetes API objects rather than in-process memory.

Error handling#

  • Style: Mixed — sentinel errors for well-known conditions, fmt.Errorf with %w for context wrapping, custom typed errors for domain-specific semantics
  • fmt.Errorf %w: 348 occurrences — dominant wrapping mechanism throughout the codebase
  • Sentinel errors: Defined in dedicated errors.go files per package (e.g., pkg/trustedresources/errors.go, pkg/trustedresources/verifier/errors.go). Pattern: var ErrXxx = errors.New("...") exported at package level. Callers use errors.Is(err, pkg.ErrXxx).
  • Custom typed errors: A distinctive Go idiom used in pkg/entrypoint:
    • type ContextError string — wraps context error messages as a named string type; ErrContextCanceled = ContextError(context.Canceled.Error()) allows typed comparison via errors.Is
    • type SkipError string — signals that a step was skipped (predecessor failed); caught by the reconciler to update status rather than fail the TaskRun
    • type DebugBeforeStepError string — signals breakpoint-induced skipping
    • type MessageLengthError string (in pkg/termination) — signals termination message overflow
    • All implement Error() string by converting to string. This allows errors.Is matching on the typed value directly.
  • Wrapping approach: fmt.Errorf("doing X: %w", err) throughout — no pkg/errors, no custom wrap functions
  • Examples:
    • cmd/entrypoint/subcommands/decode_script.go:35fmt.Errorf("error decoding script file %q: %w", scriptPath, err)
    • pkg/reconciler/pipeline/dag/dag.go:84fmt.Errorf("cycle detected; %w", err) — note semicolon separator style
    • pkg/trustedresources/errors.go:22 — dedicated sentinel file with 4 exported errors
  • Error types defined: SubcommandError, InvalidRuntimeObjectError, DataAccessError, ContextError, SkipError, DebugBeforeStepError, MessageLengthError, VerificationResult (with typed outcome enum)

Configuration pattern#

  • Approach: Context-carried config struct (Knative configmap.Store pattern)
  • Mechanism: Kubernetes ConfigMaps are watched live; on each change the config.Store updates an in-memory struct attached to context.Context. Callers use config.FromContextOrDefaults(ctx) — never blocking, always returns a valid struct.
  • Example: pkg/pod/pod.go:158-165 — 6 consecutive config.FromContextOrDefaults(ctx).FeatureFlags.* calls at the top of Build() — no argument passing needed; config flows through context
  • Binary flags: Image references and thread counts set via flag package at startup; no Viper. Environment variable THREADS_PER_CONTROLLER supplemented by flag.
  • Assessment: Elegant for hot-reload of ConfigMaps without restart. The downside is that context.Context becomes a grab-bag — it carries logger, config, clients, and cancellation simultaneously.

Dependency injection#

  • Approach: Service locator via context.Context (Knative injection pattern)
  • Evidence: cmd/controller/main.go — Kubernetes clients and informers are registered into context before controller factories run. Controllers extract dependencies with typed Get(ctx) functions: kubeclient.Get(ctx), taskruninformer.Get(ctx), logging.FromContext(ctx). No wire, dig, or fx.
  • Assessment: Functional but non-standard. It trades explicit constructor parameters for implicit context threading. This makes dependency graphs invisible to static analysis and increases test setup complexity. It is idiomatic for Knative-based operators but unusual outside that ecosystem.

Other notable patterns#

Transformer (functional decorator)#

  • Where: pkg/pod/pod.go:145type Transformer func(*corev1.Pod) (*corev1.Pod, error)
  • How: Builder.Build(ctx, taskRun, taskSpec, transformers ...Transformer) applies each transformer in sequence after building the base Pod. Separate packages contribute transformers: pkg/internal/computeresources.NewTransformer, pkg/internal/affinityassistant.NewTransformer, pkg/internal/defaultresourcerequirements.NewTransformer
  • Assessment: Clean extensibility without subclassing. New pod mutations are added by writing a new Transformer function and passing it at the call site. Avoids a growing switch/case in Build. This is a well-executed functional decorator pattern.

Builder (explicit struct + Build method)#

  • Where: pkg/pod.Builder (struct with Build method) and pkg/reconciler/pipeline/dag.Build (package-level function)
  • How: pod.Builder holds shared configuration (image refs, KubeClient, entrypoint cache); Build is called per TaskRun. dag.Build(tasks Tasks, deps map[string][]string) (*Graph, error) is a pure-function builder with no state.
  • Assessment: Both are clear and testable. The dag package’s stateless Build function (no constructor needed) is particularly clean.

Interface-driven testability (small interfaces)#

  • Where: pkg/entrypointWaiter and Runner interfaces abstract file polling and process execution; pkg/remoteresolutionRequester interface abstracts CRD-based resolution
  • How: Production code depends only on the interface; tests inject fakes (fakeErrorWaiter, fakeErrorRunner)
  • Assessment: Classic Go interface pattern. Interfaces are defined at the consumer side, are small (1-3 methods), and enable full unit testing without Kubernetes dependencies.

String-typed sentinel errors#

  • Where: pkg/entrypoint/entrypointer.go:77-120
  • How: type ContextError string — a named type over string that implements error. Sentinel values like ErrContextCanceled = ContextError(context.Canceled.Error()) are compared with errors.Is. Because ContextError is a distinct type, an errors.Is check only matches the typed sentinel, not a plain string with the same value.
  • Assessment: Unusual but valid. It achieves typed error discrimination without a struct. The string representation is human-readable. The tradeoff is that error messages are hardcoded; changing the string breaks equality.

Pure functional DAG (no Kubernetes dependencies)#

  • Where: pkg/reconciler/pipeline/dag
  • How: The entire DAG package — Graph, Node, Task interface, Build, GetCandidateTasks, cycle detection — has zero Kubernetes imports. It operates on abstract Task and Tasks interfaces.
  • Assessment: Excellent separation of concerns. The scheduling algorithm is independently testable and understandable without Kubernetes knowledge. A textbook example of isolating domain logic from infrastructure.

Generics (limited, utility use)#

  • Count: 4 generic functions found; not pervasive
  • Examples:
    • pkg/apis/pipeline/pod/template.go:294func mergeByName[T any](base, overrides []T) []T — merges container slices by name field (uses reflection internally or similar)
    • pkg/reconciler/pipelinerun/resources/apply.go:194func paramExists[T paramValue](paramName string, bucket map[string]T) bool — parameterized over concrete param value types
    • pkg/entrypoint/entrypointer_test.go:2313func ptr[T any](value T) *T — test helper
  • Assessment: Conservative, appropriate use. Generics are applied only where they eliminate repetition in type-safe ways; the codebase does not over-generalize.

Table-driven tests (inferred from testing patterns)#

  • Prevalent across the codebase — the standard Go approach; reconciler tests follow Knative’s reconcilertesting.TableTest pattern with named test cases.

CloudEvents / Kubernetes event recording (observer pattern)#

  • Where: Reconcilers call r.Recorder.Eventf(resource, eventType, reason, msg) — Kubernetes EventRecorder pattern
  • How: Structured events are emitted to the Kubernetes event log on state transitions (TaskRun started, failed, succeeded). CloudEvents are emitted separately via pkg/cloudevent for external consumers.
  • Assessment: Two-track notification: Kubernetes events for cluster-internal observability; CloudEvents for external integration (e.g., Tekton Triggers). Clean separation.