Documentation
¶
Overview ¶
Package taskflow is an optional DAG runner for containerkit.
Hand the same *kit.Container, kit.Build, and *kit.Compose values you already have to a Flow, plus ordinary Go functions. kit and client do not import this package; a program that never imports taskflow never downloads go-taskflow.
import "github.com/aptd3v/containerkit/pkg/taskflow"
Graph ¶
New names the graph and holds an optional *client.Client. Add steps, wire edges, then call Run once. Independent steps whose predecessors are done run concurrently. Run.Concurrency is the worker pool (0 means GOMAXPROCS, raised above the number of subflows).
flow := taskflow.New("ci", cli)
build := flow.Build("build", kit.NewBuild().Dockerfile(df), &client.ImageBuild{Tags: []string{"app:ci"}})
up := flow.Compose("up-db", db, &kit.Up{Detach: true, Wait: true})
test := flow.Container("test", kit.NewContainer().Image("app:ci"), nil).Retry(2)
test.DependsOn(up.DependsOn(build))
defer flow.Remove(ctx)
err := flow.Run(ctx, &taskflow.Run{Writer: os.Stdout})
cli may be nil when every step is Task or Compose. Container and Build need a client (ErrNoClient otherwise).
Steps ¶
Flow and Subflow share these constructors. Every one returns *Task.
- Task(name, fn) — fn(ctx) error
- Build(name, kit.Build, *client.ImageBuild) — Engine image build
- Container(name, *kit.Container, *ContainerRun) — remove, create, start, wait for exit
- Compose(name, *kit.Compose, cmd) — *kit.Up, *kit.Logs, … or func(ctx, app) error
- Subflow(name, instantiate) — nested graph; instantiate runs when the subflow starts
- If(name, pred) — boolean branch (Then / Else)
- Condition(name, pred) — indexed branch (Precede order)
Container is a one-shot job. A non-zero exit fails the step. Restart policies (always, unless-stopped, on-failure) and AutoRemove are rejected: this step records the first exit, then copies logs. Long-running services belong in Compose with *kit.Up; flaky jobs use Retry. The step writes Name and containerkit.* labels onto the *kit.Container you pass.
Compose cmd is a *kit option. The struct is copied; a nil Writer becomes Run.Writer. Use a func for Events, LoadProject, or options that depend on earlier steps.
Edges ¶
a.Precede(b) is b.DependsOn(a): A then B. Priority only orders the ready queue; it does not override edges.
Failure ¶
A failed or skipped step skips its successors. Siblings keep running. Run joins every step error.
Always runs a step even when a predecessor failed (teardown inside the graph). Retry(n) reruns a failing step n more times. Panics and a canceled context are not retried.
If is skipped when a predecessor failed, and then neither branch runs. Condition still runs after a failed predecessor, which is what makes a retry cycle work: a non-condition node is the first entry, and the Condition sits on the back edge. For retrying one step, Retry is simpler than a cycle.
Prefer defer flow.Remove for end-of-run teardown so List can still see the stack. Use a *kit.Down step when the graph itself must bring the project down (and Always if that must happen after a failure).
Observe ¶
Dump writes DOT. Run.Profile is flamegraph text; Run.Trace is Chrome Trace JSON. Run.Watch is step lifecycle (start, done, skip, retry, error). Run.Log is step-scoped bytes (build stream, container logs, compose progress).
Container and Compose steps stamp containerkit.flow / task / depends / precedes. List is docker ps -a for that flow label; call it before Remove. Remove Downs every *kit.Compose the graph referenced, then deletes leftover labeled containers.
The full guide is the README in this directory. A runnable CI graph is examples/taskflow.
Example ¶
package main
import (
"context"
"fmt"
"github.com/aptd3v/containerkit/pkg/taskflow"
)
func main() {
flow := taskflow.New("demo", nil)
hello := flow.Task("hello", func(context.Context) error {
fmt.Println("hello")
return nil
})
done := flow.Task("done", func(context.Context) error {
fmt.Println("done")
return nil
})
hello.Precede(done)
if err := flow.Run(context.Background(), nil); err != nil {
fmt.Println(err)
}
}
Output: hello done
Index ¶
- Constants
- Variables
- func List(ctx context.Context, cli *client.Client, flowName string) ([]response.ContainerSummary, error)
- type ContainerRun
- type Event
- type EventKind
- type Flow
- func (f *Flow) Build(name string, b kit.Build, opt *client.ImageBuild) *Task
- func (f *Flow) Compose(name string, app *kit.Compose, cmd any) *Task
- func (f *Flow) Condition(name string, predict func(context.Context) uint) *Task
- func (f *Flow) Container(name string, ctr *kit.Container, opt *ContainerRun) *Task
- func (f *Flow) Dump(w io.Writer) error
- func (f *Flow) If(name string, pred func(context.Context) bool) *Task
- func (f *Flow) List(ctx context.Context) ([]response.ContainerSummary, error)
- func (f *Flow) Name() string
- func (f *Flow) Remove(ctx context.Context) error
- func (f *Flow) Run(ctx context.Context, opt *Run) error
- func (f *Flow) Subflow(name string, instantiate func(*Subflow)) *Task
- func (f *Flow) Task(name string, fn func(context.Context) error) *Task
- type Priority
- type Run
- type Subflow
- func (s *Subflow) Build(name string, b kit.Build, opt *client.ImageBuild) *Task
- func (s *Subflow) Compose(name string, app *kit.Compose, cmd any) *Task
- func (s *Subflow) Condition(name string, predict func(context.Context) uint) *Task
- func (s *Subflow) Container(name string, ctr *kit.Container, opt *ContainerRun) *Task
- func (s *Subflow) If(name string, pred func(context.Context) bool) *Task
- func (s *Subflow) Subflow(name string, instantiate func(*Subflow)) *Task
- func (s *Subflow) Task(name string, fn func(context.Context) error) *Task
- type Task
- func (t *Task) Always() *Task
- func (t *Task) DependsOn(tasks ...*Task) *Task
- func (t *Task) Else(tasks ...*Task) *Task
- func (t *Task) Err() error
- func (t *Task) ExitCode() int
- func (t *Task) ID() string
- func (t *Task) Name() string
- func (t *Task) Precede(tasks ...*Task) *Task
- func (t *Task) Priority(p Priority) *Task
- func (t *Task) Retry(n int) *Task
- func (t *Task) Then(tasks ...*Task) *Task
- type TaskError
Examples ¶
Constants ¶
const ( // LabelFlow is the Docker label for the flow name. LabelFlow = "containerkit.flow" // LabelTask is the Docker label for the step name. LabelTask = "containerkit.task" // LabelDepends lists predecessor container names, sorted and comma-separated. LabelDepends = "containerkit.depends" // LabelPrecedes lists successor container names, sorted and comma-separated. LabelPrecedes = "containerkit.precedes" )
Variables ¶
var ( // ErrNoClient is returned when a Container or Build step runs without a client. ErrNoClient = errors.New("docker client is nil") // ErrAlreadyRun is returned when Run is called a second time on the same Flow. ErrAlreadyRun = errors.New("already run") )
Functions ¶
Types ¶
type ContainerRun ¶
type ContainerRun struct {
// Logs receives the container's stdout and stderr after it exits. Nil
// means Run.Writer. Use io.Discard to silence one step.
Logs io.Writer
}
ContainerRun is options for a Container step. Nil means defaults.
type Event ¶
type Event struct {
// Task is the step. Step names are not unique across subflows; compare Task.
Task *Task
Name string
Kind EventKind
// Err is set for EventError and EventRetry.
Err error
// Attempt is the 1-based attempt that just failed. Set for EventRetry.
Attempt int
Duration time.Duration
}
Event is one step lifecycle update. Watch may be called from several goroutines.
type Flow ¶
type Flow struct {
// contains filtered or unexported fields
}
Flow is a named task graph. One Run per Flow.
func New ¶
New returns a Flow. name is the graph name and the flow label value. cli may be nil when the flow has only Task and Compose steps.
func (*Flow) Compose ¶
Compose adds a step that runs a compose command. cmd is a *kit option (*kit.Up, *kit.Down, *kit.Logs, …) or a func(context.Context, *kit.Compose) error for Events, LoadProject, or options that depend on earlier steps. The option is copied so the caller's value is not mutated. A nil Writer on the option means Run.Writer. Prefer defer flow.Remove for end-of-run teardown so List can still see the stack; use a *kit.Down step when the graph itself must bring the project down (and Always if that must happen after a failure).
func (*Flow) Condition ¶
Condition adds an indexed branch. The returned index selects the successor in Precede order. A Condition runs even when a predecessor failed, which is what makes retry cycles work: a cycle needs a non-condition predecessor as the first entry, then a Condition on the back edge. An index with no successor fails the step instead of panicking; nothing runs after it. Use If to gate on a predecessor's success.
func (*Flow) Container ¶
Container adds a one-shot container step: remove by name, create, start, wait for exit. A non-zero exit fails the step. Restart policies (always, unless-stopped, on-failure) and AutoRemove are rejected: this step records the first exit, then copies logs; use Compose with *kit.Up for a long-running service and Retry for a flaky job. The step writes to ctr: it sets Name when empty and adds the containerkit.* labels.
func (*Flow) If ¶
If adds a boolean branch. Then successors run when pred returns true, Else successors when it returns false. A missing branch is a no-op. Unlike Condition, an If is skipped when a predecessor failed, and then neither branch runs.
func (*Flow) Remove ¶
Remove downs every Compose project the graph referenced, then force-removes Container-step IDs and any leftover containers stamped with this flow's name.
func (*Flow) Run ¶
Run executes the graph. Nil opt uses defaults. The returned error joins every failed step's TaskError.
type Priority ¶
type Priority uint
Priority is scheduler queue order. Zero leaves go-taskflow's default.
type Run ¶
type Run struct {
// Concurrency is the worker count. Zero means GOMAXPROCS; the value is
// raised above the number of known subflows.
Concurrency uint
// Writer receives build output, container output, and Compose output.
// Nil means os.Stdout.
Writer io.Writer
// Profile receives flamegraph text when the graph finishes. Nil disables the profiler.
Profile io.Writer
// Trace receives Chrome Trace JSON when the graph finishes. Nil disables the tracer.
Trace io.Writer
// Watch is called from worker goroutines as each step starts, finishes,
// retries, skips, or fails.
Watch func(Event)
// Log is called with step-scoped output (image build streams, container
// logs, compose output). May be called from worker goroutines with
// partial writes.
Log func(step string, p []byte)
}
Run is options for Flow.Run. Nil means defaults.
type Subflow ¶
type Subflow struct {
// contains filtered or unexported fields
}
Subflow is a nested graph. It has the same step constructors as Flow.
func (*Subflow) Condition ¶
Condition adds an indexed branch inside the subflow. See Flow.Condition.
func (*Subflow) Container ¶
Container adds a one-shot container step to the subflow. See Flow.Container.
type Task ¶
type Task struct {
// contains filtered or unexported fields
}
Task is one node in a Flow or Subflow.
func (*Task) Always ¶
Always runs t even when a predecessor failed or was skipped. Use it for teardown. The upstream failure still propagates: successors of t are skipped as if t had failed.
func (*Task) Else ¶
Else adds successors that run when an If step returns false. On any other step it does nothing.
func (*Task) Err ¶
Err is the step error after Run, if any. For a Subflow it reports only the subflow's own error; inner step errors are on the inner tasks and in the error returned by Run.
func (*Task) Precede ¶
Precede makes tasks depend on t. For a Condition, the order is the branch index. For an If, Precede is the same as Then.