Files
colima/cli/chain.go
2025-08-20 10:52:35 +01:00

163 lines
3.5 KiB
Go

package cli
import (
"context"
"fmt"
"io"
"time"
log "github.com/sirupsen/logrus"
)
// CtxKeyQuiet is the context key to mute the chain.
var CtxKeyQuiet = struct{ key string }{key: "quiet"}
// errNonFatal is a non fatal error
type errNonFatal struct {
err error
}
// Error implements error
func (e errNonFatal) Error() string { return e.err.Error() }
// ErrNonFatal creates a non-fatal error for a command chain.
// A warning would be printed instead of terminating the chain.
func ErrNonFatal(err error) error {
return errNonFatal{err}
}
// New creates a new runner instance.
func New(name string) CommandChain {
return &namedCommandChain{
name: name,
}
}
type cFunc struct {
f func() error
s string
}
// CommandChain is a chain of commands.
// commands are executed in order.
type CommandChain interface {
// Init initiates a new runner using the current instance.
Init(ctx context.Context) *ActiveCommandChain
// Logger returns the instance logger.
Logger(ctx context.Context) *log.Entry
}
var _ CommandChain = (*namedCommandChain)(nil)
type namedCommandChain struct {
name string
log *log.Entry
}
func (n *namedCommandChain) Logger(ctx context.Context) *log.Entry {
if quiet, _ := ctx.Value(CtxKeyQuiet).(bool); quiet {
l := log.New()
l.SetOutput(io.Discard)
return l.WithContext(ctx)
}
if n.log == nil {
n.log = log.WithField("context", n.name).WithContext(ctx)
}
return n.log
}
func (n *namedCommandChain) Init(ctx context.Context) *ActiveCommandChain {
return &ActiveCommandChain{
log: n.Logger(ctx),
}
}
// ActiveCommandChain is an active command chain.
type ActiveCommandChain struct {
funcs []cFunc
lastStage string
log *log.Entry
executing bool
}
// Logger returns the logger for the command chain.
func (a *ActiveCommandChain) Logger() *log.Entry { return a.log }
// Add adds a new function to the runner.
func (a *ActiveCommandChain) Add(f func() error) {
a.funcs = append(a.funcs, cFunc{f: f})
}
// Stage sets the current stage of the runner.
func (a *ActiveCommandChain) Stage(s string) {
if a.executing {
a.log.Println(s, "...")
return
}
a.funcs = append(a.funcs, cFunc{s: s})
}
// Stagef is like stage with string format.
func (a *ActiveCommandChain) Stagef(format string, s ...any) {
f := fmt.Sprintf(format, s...)
a.Stage(f)
}
// Exec executes the command chain.
// The first errored function terminates the chain and the
// error is returned. Otherwise, returns nil.
func (a *ActiveCommandChain) Exec() error {
a.executing = true
defer func() { a.executing = false }()
for _, f := range a.funcs {
if f.f == nil {
if f.s != "" {
a.log.Println(f.s, "...")
a.lastStage = f.s
}
continue
}
// success
err := f.f()
if err == nil {
continue
}
// warning
if _, ok := err.(errNonFatal); ok {
if a.lastStage == "" {
a.log.Warnln(err)
} else {
a.log.Warnln(fmt.Errorf("error at '%s': %w", a.lastStage, err))
}
continue
}
// error
if a.lastStage == "" {
return err
}
return fmt.Errorf("error at '%s': %w", a.lastStage, err)
}
return nil
}
// Retry retries `f` up to `count` times at interval.
// If after `count` attempts there is an error, the command chain is terminated with the final error.
// retryCount starts from 1.
func (a *ActiveCommandChain) Retry(stage string, interval time.Duration, count int, f func(retryCount int) error) {
a.Add(func() (err error) {
var i int
for err = f(i + 1); i < count && err != nil; i, err = i+1, f(i+1) {
if stage != "" {
a.log.Println(stage, "...")
}
time.Sleep(interval)
}
return err
})
}