From 128c424fe80b4db9d67bdfae5431ee8277ce72db Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Wed, 18 Mar 2026 13:02:30 +0000 Subject: [PATCH] spindle/engines: add dummy engine that logs to stdout Useful during local testing. Signed-off-by: Anirudh Oppiliappan --- .gitignore | 1 + spindle/engines/dummy/engine.go | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ spindle/server.go | 2 ++ 3 file(s) changed, 95 insertion(s)(+), 0 deletion(s)(-) diff --git a/.gitignore b/.gitignore --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ id_dsa id_ed25519 *.pem *.key +.claude/ diff --git a/spindle/engines/dummy/engine.go b/spindle/engines/dummy/engine.go new file mode 100644 --- /dev/null +++ b/spindle/engines/dummy/engine.go @@ -0,0 +1,92 @@ +package dummy + +import ( + "context" + "fmt" + "log/slog" + "time" + + "gopkg.in/yaml.v3" + "tangled.org/core/api/tangled" + "tangled.org/core/spindle/models" + "tangled.org/core/spindle/secrets" +) + +// DummyEngine is a no-op engine that logs all lifecycle events via slog and writes +// step output to the workflow logger. Useful for testing pipeline plumbing +// without a real execution backend. +type DummyEngine struct { + l *slog.Logger +} + +func New(l *slog.Logger) *DummyEngine { + return &DummyEngine{l: l.With("engine", "dummy")} +} + +type Step struct { + name string + kind models.StepKind + command string +} + +func (s Step) Name() string { return s.name } +func (s Step) Command() string { return s.command } +func (s Step) Kind() models.StepKind { return s.kind } + +func (e *DummyEngine) InitWorkflow(twf tangled.Pipeline_Workflow, _ tangled.Pipeline) (*models.Workflow, error) { + dwf := &struct { + Steps []struct { + Name string `yaml:"name"` + Command string `yaml:"command"` + } `yaml:"steps"` + Environment map[string]string `yaml:"environment"` + }{} + + if err := yaml.Unmarshal([]byte(twf.Raw), dwf); err != nil { + return nil, err + } + + wf := &models.Workflow{ + Name: twf.Name, + Environment: dwf.Environment, + } + for _, ds := range dwf.Steps { + wf.Steps = append(wf.Steps, Step{ + name: ds.Name, + kind: models.StepKindUser, + command: ds.Command, + }) + } + + e.l.Info("workflow initialised", "name", twf.Name, "steps", len(wf.Steps)) + return wf, nil +} + +func (e *DummyEngine) SetupWorkflow(_ context.Context, wid models.WorkflowId, wf *models.Workflow, wfLogger models.WorkflowLogger) error { + e.l.Info("setting up workflow", "wid", wid) + + setupStep := Step{name: "dummy setup", kind: models.StepKindSystem} + const setupIdx = -1 + + wfLogger.ControlWriter(setupIdx, setupStep, models.StepStatusStart).Write([]byte{0}) + defer wfLogger.ControlWriter(setupIdx, setupStep, models.StepStatusEnd).Write([]byte{0}) + + fmt.Fprintf(wfLogger.DataWriter(setupIdx, "stdout"), "dummy engine: workflow %q ready", wf.Name) + return nil +} + +func (e *DummyEngine) WorkflowTimeout() time.Duration { + return 5 * time.Minute +} + +func (e *DummyEngine) DestroyWorkflow(_ context.Context, wid models.WorkflowId) error { + e.l.Info("destroying workflow", "wid", wid) + return nil +} + +func (e *DummyEngine) RunStep(_ context.Context, wid models.WorkflowId, w *models.Workflow, idx int, _ []secrets.UnlockedSecret, wfLogger models.WorkflowLogger) error { + step := w.Steps[idx] + e.l.Info("running step", "wid", wid, "step", step.Name(), "command", step.Command()) + fmt.Fprintf(wfLogger.DataWriter(idx, "stdout"), "$ %s", step.Command()) + return nil +} diff --git a/spindle/server.go b/spindle/server.go --- a/spindle/server.go +++ b/spindle/server.go @@ -24,6 +24,7 @@ "tangled.org/core/rbac" "tangled.org/core/spindle/config" "tangled.org/core/spindle/db" "tangled.org/core/spindle/engine" + "tangled.org/core/spindle/engines/dummy" "tangled.org/core/spindle/engines/nixery" "tangled.org/core/spindle/models" "tangled.org/core/spindle/queue" @@ -334,6 +335,7 @@ } s, err := New(ctx, cfg, map[string]models.Engine{ "nixery": nixeryEng, + "dummy": dummy.New(log.FromContext(ctx)), }) if err != nil { return err -- tangled.sh