Skip to content

executor

import "github.com/alehatsman/mooncake/internal/executor"

Package executor provides the execution engine for mooncake configuration steps.

Package executor implements the core execution engine for mooncake configuration plans.

The executor is responsible for:

  • Loading and validating configuration plans
  • Expanding steps (loops, includes, presets)
  • Evaluating conditions (when, unless, creates)
  • Dispatching actions to handlers
  • Managing execution context and variables
  • Tracking results and statistics
  • Emitting events for observability
  • Handling dry-run mode
  • Supporting privilege escalation (sudo/become)

Architecture

The executor follows a pipeline architecture:

Plan Loading → Step Expansion → Condition Evaluation → Action Dispatch → Result Handling

Each step goes through:

  1. Pre-execution: Check when/unless/creates, apply tags filter
  2. Variable processing: Merge step vars into context
  3. Loop expansion: Expand with_items/with_filetree into multiple executions
  4. Action execution: Dispatch to handler or legacy implementation
  5. Post-execution: Evaluate changed_when/failed_when, register results
  6. Event emission: Publish lifecycle events

Execution Context

ExecutionContext carries all state needed during execution:

  • Variables: Step vars, global vars, facts, registered results
  • Template: Jinja2-like template renderer
  • Evaluator: Expression evaluator for conditions
  • Logger: Structured logging (TUI or text)
  • PathUtil: Path resolution and expansion
  • EventPublisher: Event emission for observability
  • Stats: Execution statistics (total, success, failed, changed, skipped)

Action Dispatch

Actions are dispatched through two paths:

  1. Handler-based (new): Look up handler in actions.Registry, call handler.Execute()
  2. Legacy: Direct executor methods (HandleShell, HandleFile, etc.)

The executor prefers handlers when available, falling back to legacy for non-migrated actions.

Idempotency

The executor enforces idempotency through:

  • creates: Skip if path exists
  • unless: Skip if command succeeds
  • changed_when: Custom change detection
  • Handler implementations: Built-in state checking

Dry\-Run Mode

When DryRun is true:

  • No actual changes are made to the system
  • Handlers log what would happen
  • Template rendering still occurs (validates syntax)
  • File existence checks are performed (read-only)
  • Statistics track what would have changed

Error Handling

Errors are wrapped with context using custom error types:

  • RenderError: Template rendering failures (field + cause)
  • EvaluationError: Expression evaluation failures (expression + cause)
  • CommandError: Command execution failures (command + exit code)
  • FileOperationError: File operation failures (path + operation + cause)
  • StepValidationError: Configuration validation failures
  • SetupError: Infrastructure/environment setup failures

Use errors.Is() and errors.As() for programmatic error inspection.

Usage Example

// Load configuration
steps, err := config.ReadConfig("config.yml")
if err != nil {
    return err
}

// Create executor
log := logger.NewTextLogger()
exec := NewExecutor(log)

// Execute with options
result, err := exec.Execute(config.Plan{Steps: steps}, ExecuteOptions{
    DryRun: false,
    Tags: []string{"setup", "deploy"},
    Variables: map[string]interface{}{
        "environment": "production",
    },
})

// Check results
if !result.Success {
    log.Errorf("Execution failed: %d failed steps", result.FailedSteps)
}
log.Infof("Summary: %d changed, %d unchanged, %d failed",
    result.ChangedSteps, result.SuccessSteps-result.ChangedSteps, result.FailedSteps)

Index

func AddGlobalVariables

func AddGlobalVariables(variables map[string]interface{})

AddGlobalVariables injects system facts into the variables map. This makes facts like ansible_os_family, ansible_distribution, etc. available during planning.

func CheckIdempotencyConditions

func CheckIdempotencyConditions(step config.Step, ec *ExecutionContext) (bool, string, error)

CheckIdempotencyConditions evaluates creates and unless conditions for shell steps. Returns (shouldSkip bool, reason string, error)

INTERNAL: This function is exported for testing purposes only and is not part of the public API. It may change or be removed in future versions without notice.

func CheckSkipConditions

func CheckSkipConditions(step config.Step, ec *ExecutionContext) (bool, string, error)

CheckSkipConditions evaluates whether a step should be skipped based on conditional expressions and tag filters.

It first evaluates the step's "when" condition (if present), which is an expression that must evaluate to true for the step to execute. If the condition evaluates to false, the step is skipped with reason "when".

Next, it checks if the step should be skipped based on tag filtering. If the execution context has a tags filter and the step's tags don't match, it's skipped with reason "tags".

Returns:

  • shouldSkip: true if the step should be skipped
  • skipReason: "when" or "tags" indicating why the step was skipped (empty if not skipped)
  • error: any error encountered while evaluating conditions

INTERNAL: This function is exported for testing purposes only and is not part of the public API. It may change or be removed in future versions without notice.

func DispatchStepAction

func DispatchStepAction(step config.Step, ec *ExecutionContext) error

DispatchStepAction executes the appropriate handler based on step type. All actions are now handled through the actions registry.

INTERNAL: This function is exported for testing purposes only and is not part of the public API. It may change or be removed in future versions without notice.

func ExecutePlan

func ExecutePlan(p *plan.Plan, sudoPass string, dryRun bool, log logger.Logger, publisher events.Publisher) error

ExecutePlan executes a pre-compiled plan. Emits events through the provided publisher for all execution progress.

func ExecuteStep

func ExecuteStep(step config.Step, ec *ExecutionContext) error

ExecuteStep executes a single configuration step within the given execution context.

func ExecuteSteps

func ExecuteSteps(steps []config.Step, ec *ExecutionContext) error

ExecuteSteps executes a sequence of configuration steps within the given execution context.

func GetStepDisplayName

func GetStepDisplayName(step config.Step, ec *ExecutionContext) (string, bool)

GetStepDisplayName determines the display name to show for a step in logs and output.

The function follows a priority order to determine the name:

  1. If executing within a with_filetree loop, uses action + destination path
  2. If executing within a with_items loop, uses the string representation of the item
  3. Otherwise, uses the step's configured Name field

Returns:

  • displayName: the name to display for this step
  • hasName: true if a name was found, false if the step is anonymous

INTERNAL: This function is exported for testing purposes only and is not part of the public API. It may change or be removed in future versions without notice.

func HandleVars

func HandleVars(step config.Step, ec *ExecutionContext) error

HandleVars processes the vars field of a step, rendering templates and merging into the execution context.

INTERNAL: This function is exported for testing purposes only and is not part of the public API. It may change or be removed in future versions without notice.

func HandleWhenExpression

func HandleWhenExpression(step config.Step, ec *ExecutionContext) (bool, error)

HandleWhenExpression evaluates the when condition and returns whether the step should be skipped. Returns (shouldSkip bool, error).

INTERNAL: This function is exported for testing purposes only and is not part of the public API. It may change or be removed in future versions without notice.

func MarkStepFailed

func MarkStepFailed(result *Result, step config.Step, ec *ExecutionContext)

MarkStepFailed marks a result as failed and registers it if needed. The caller is responsible for returning an appropriate error.

INTERNAL: This function is exported for testing purposes only and is not part of the public API. It may change or be removed in future versions without notice.

func ParseFileMode

func ParseFileMode(modeStr string, defaultMode os.FileMode) os.FileMode

ParseFileMode parses a mode string (e.g., "0644") into os.FileMode. Returns default mode if mode is empty or invalid.

INTERNAL: This function is exported for testing purposes only and is not part of the public API. It may change or be removed in future versions without notice.

func ShouldSkipByTags

func ShouldSkipByTags(step config.Step, ec *ExecutionContext) bool

ShouldSkipByTags determines if a step should be skipped based on tag filtering. Returns true if the step should be skipped, false otherwise.

INTERNAL: This function is exported for testing purposes only and is not part of the public API. It may change or be removed in future versions without notice.

func Start

func Start(StartConfig StartConfig, log logger.Logger, publisher events.Publisher) error

Start begins execution of a mooncake configuration with the given settings. Always goes through the planner to expand loops, includes, and variables. Emits events through the provided publisher for all execution progress.

type AssertionError

AssertionError represents an assertion verification failure. Unlike other errors, assertions are expected to fail when conditions aren't met.

type AssertionError struct {
    Type     string // "command", "file", "http"
    Expected string // What was expected
    Actual   string // What was found
    Details  string // Additional context (optional)
    Cause    error  // Underlying error (optional)
}

func (*AssertionError) Error

func (e *AssertionError) Error() string

func (*AssertionError) Unwrap

func (e *AssertionError) Unwrap() error

type CommandError

CommandError represents a command execution failure

type CommandError struct {
    ExitCode int
    Timeout  bool
    Duration string
    Cause    error // Optional underlying error (e.g., exec.ExitError, OS errors)
}

func (*CommandError) Error

func (e *CommandError) Error() string

func (*CommandError) Unwrap

func (e *CommandError) Unwrap() error

type EvaluationError

EvaluationError represents an expression evaluation failure

type EvaluationError struct {
    Expression string
    Cause      error
}

func (*EvaluationError) Error

func (e *EvaluationError) Error() string

func (*EvaluationError) Unwrap

func (e *EvaluationError) Unwrap() error

type ExecutionContext

ExecutionContext holds all state needed to execute a step or sequence of steps.

The context is designed to be copied when entering nested execution scopes (includes, loops). Most fields are copied by value, but certain fields use pointers to maintain shared state across the entire execution tree.

Field categories:

  • Configuration: Variables, CurrentDir, CurrentFile (copied on nested contexts)
  • Display state: Level, CurrentIndex, TotalSteps (modified for each scope)
  • Execution settings: Logger, SudoPass, Tags, DryRun (shared across contexts)
  • Global counters: Pointers that accumulate across all contexts
  • Dependencies: Shared service instances
type ExecutionContext struct {
    // Variables contains template variables available to steps.
    // Copied on context copy so nested contexts can have their own variables (e.g., loop items).
    Variables map[string]interface{}

    // CurrentDir is the directory containing the current config file.
    // Used for resolving relative paths in include, template src, etc.
    CurrentDir string

    // CurrentFile is the absolute path to the current config file being executed.
    // Used for error messages and debugging.
    CurrentFile string

    // Level tracks nesting depth for display indentation.
    // 0 = root config, increments by 1 for each include or loop level.
    Level int

    // CurrentIndex is the 0-based index of the current step within the current scope.
    // Resets to 0 when entering includes or loops.
    CurrentIndex int

    // TotalSteps is the number of steps in the current execution scope.
    // Updated when entering includes or loops to reflect the new scope size.
    TotalSteps int

    // Logger handles all output, configured with padding based on Level.
    Logger logger.Logger

    // SudoPass is the password used for steps with become: true.
    // Empty string if not provided via --sudo-pass flag.
    SudoPass string

    // Tags filters which steps execute (empty = all steps execute).
    // Steps without matching tags are skipped when this is non-empty.
    Tags []string

    // DryRun when true prevents any system changes (preview mode).
    // Commands are not executed, files are not created, templates are not rendered.
    DryRun bool

    // Stats holds shared execution statistics counters.
    // SHARED via pointer - all contexts update the same counters.
    Stats *ExecutionStats

    // Template renders template strings with variable substitution.
    // SHARED across all contexts - same instance used everywhere.
    Template template.Renderer

    // Evaluator evaluates when condition expressions.
    // SHARED across all contexts - same instance used everywhere.
    Evaluator expression.Evaluator

    // PathUtil expands paths with tilde and variable substitution.
    // SHARED across all contexts - same instance used everywhere.
    PathUtil *pathutil.PathExpander

    // FileTree walks directory trees for with_filetree.
    // SHARED across all contexts - same instance used everywhere.
    FileTree *filetree.Walker

    // Redactor redacts sensitive values (passwords) from log output.
    // SHARED across all contexts - same instance used everywhere.
    Redactor *security.Redactor

    // EventPublisher publishes execution events to subscribers.
    // SHARED across all contexts - same instance used everywhere.
    EventPublisher events.Publisher

    // CurrentStepID is the unique identifier for the currently executing step.
    // Used for correlating events from the same step execution.
    CurrentStepID string

    // CurrentResult holds the result of the currently executing step.
    // Handlers should set this to provide result data to event emission.
    CurrentResult *Result
}

func (*ExecutionContext) Clone

func (ec *ExecutionContext) Clone() ExecutionContext

Clone creates a new ExecutionContext for a nested execution scope (include or loop). Variables map is shallow copied, display fields are copied by value, and pointer fields remain shared across all contexts.

func (*ExecutionContext) EmitEvent

func (ec *ExecutionContext) EmitEvent(eventType events.EventType, data interface{})

EmitEvent publishes an event to all subscribers

func (*ExecutionContext) GetCurrentStepID

func (ec *ExecutionContext) GetCurrentStepID() string

GetCurrentStepID returns the current step ID.

func (*ExecutionContext) GetEvaluator

func (ec *ExecutionContext) GetEvaluator() expression.Evaluator

GetEvaluator returns the expression evaluator.

func (*ExecutionContext) GetEventPublisher

func (ec *ExecutionContext) GetEventPublisher() events.Publisher

GetEventPublisher returns the event publisher.

func (*ExecutionContext) GetLogger

func (ec *ExecutionContext) GetLogger() logger.Logger

GetLogger returns the logger.

func (*ExecutionContext) GetTemplate

func (ec *ExecutionContext) GetTemplate() template.Renderer

GetTemplate returns the template renderer.

func (*ExecutionContext) GetVariables

func (ec *ExecutionContext) GetVariables() map[string]interface{}

GetVariables returns the execution variables.

func (*ExecutionContext) HandleDryRun

func (ec *ExecutionContext) HandleDryRun(logFn func(*dryRunLogger)) bool

HandleDryRun executes dry-run logging if in dry-run mode. Returns true if in dry-run mode (caller should return early). The logFn is called with a dryRunLogger to perform logging.

func (*ExecutionContext) IsDryRun

func (ec *ExecutionContext) IsDryRun() bool

IsDryRun returns true if this is a dry-run execution.

type ExecutionStats

ExecutionStats holds shared statistics counters for execution tracking. All fields are pointers to enable shared state across nested execution contexts.

type ExecutionStats struct {
    // Global tracks total non-skipped steps across the entire execution tree
    Global *int
    // Executed counts successfully completed steps
    Executed *int
    // Skipped counts steps skipped due to when conditions or tag filtering
    Skipped *int
    // Failed counts steps that failed with errors
    Failed *int
}

func NewExecutionStats

func NewExecutionStats() *ExecutionStats

NewExecutionStats creates a new ExecutionStats with all counters initialized to zero

type FileOperationError

FileOperationError represents a file operation failure

type FileOperationError struct {
    Operation string // "create", "read", "write", "delete", "chmod", "chown", "link"
    Path      string
    Cause     error
}

func (*FileOperationError) Error

func (e *FileOperationError) Error() string

func (*FileOperationError) Unwrap

func (e *FileOperationError) Unwrap() error

type RenderError

RenderError represents a template rendering failure

type RenderError struct {
    Field string
    Cause error
}

func (*RenderError) Error

func (e *RenderError) Error() string

func (*RenderError) Unwrap

func (e *RenderError) Unwrap() error

type Result

Result represents the outcome of executing a step and can be registered to variables for use in subsequent steps via the "register" field.

Field usage varies by step type:

Shell steps:

  • Stdout: captured standard output from the command
  • Stderr: captured standard error from the command
  • Rc: exit code (0 for success, non-zero for failure)
  • Failed: true if Rc != 0
  • Changed: always true (commands are assumed to make changes)

File steps (file with state: file or directory):

  • Rc: 0 for success, 1 for failure
  • Failed: true if file/directory operation failed
  • Changed: true if file/directory was created or content modified

Template steps:

  • Rc: 0 for success, 1 for failure
  • Failed: true if template rendering or file write failed
  • Changed: true if output file was created or content changed

Variable steps (vars, include_vars):

  • All fields remain at default values (not currently used)

The Skipped field is reserved for future use but not currently set by any step type.

type Result struct {
    // Stdout contains the standard output from shell commands.
    // Only populated by shell steps.
    Stdout string `json:"stdout"`

    // Stderr contains the standard error from shell commands.
    // Only populated by shell steps.
    Stderr string `json:"stderr"`

    // Rc is the return/exit code.
    // For shell steps: the command's exit code (0 = success).
    // For file/template steps: 0 for success, 1 for failure.
    Rc  int `json:"rc"`

    // Failed indicates whether the step execution failed.
    // Set to true when shell commands exit non-zero or when file/template operations error.
    Failed bool `json:"failed"`

    // Changed indicates whether the step made modifications to the system.
    // Shell steps: always true (commands assumed to make changes).
    // File steps: true if file/directory was created or modified.
    // Template steps: true if output file was created or content changed.
    Changed bool `json:"changed"`

    // Skipped is reserved for future use to indicate skipped steps.
    // Currently not set by any step type.
    Skipped bool `json:"skipped"`

    // Timing information
    StartTime time.Time     `json:"start_time,omitempty"`
    EndTime   time.Time     `json:"end_time,omitempty"`
    Duration  time.Duration `json:"duration_ms,omitempty"` // Duration in time.Duration format
}

func NewResult

func NewResult() *Result

NewResult creates a new Result with default values.

func (*Result) RegisterTo

func (r *Result) RegisterTo(variables map[string]interface{}, name string)

RegisterTo registers this result to the variables map under the given name. The result can be accessed using nested field syntax (e.g., "result.stdout", "result.rc") in templates and when conditions.

func (*Result) SetChanged

func (r *Result) SetChanged(changed bool)

SetChanged marks whether the action made changes.

func (*Result) SetData

func (r *Result) SetData(data map[string]interface{})

SetData sets custom result data. This merges the provided data into the result's ToMap output.

func (*Result) SetFailed

func (r *Result) SetFailed(failed bool)

SetFailed marks the result as failed.

func (*Result) SetStderr

func (r *Result) SetStderr(stderr string)

SetStderr sets the stderr output.

func (*Result) SetStdout

func (r *Result) SetStdout(stdout string)

SetStdout sets the stdout output.

func (*Result) Status

func (r *Result) Status() string

Status returns a string representation of the result status.

func (*Result) ToMap

func (r *Result) ToMap() map[string]interface{}

ToMap converts Result to a map for use in template variables.

type SetupError

SetupError represents infrastructure or configuration setup failures

type SetupError struct {
    Component string // "become", "timeout", "sudo", "user", "group"
    Issue     string // What went wrong
    Cause     error  // Underlying error (optional)
}

func (*SetupError) Error

func (e *SetupError) Error() string

func (*SetupError) Unwrap

func (e *SetupError) Unwrap() error

type StartConfig

StartConfig contains configuration for starting a mooncake execution.

type StartConfig struct {
    ConfigFilePath   string
    VarsFilePath     string
    SudoPass         string // Sudo password provided directly (use SudoPassFile for better security)
    SudoPassFile     string
    AskBecomePass    bool
    InsecureSudoPass bool
    Tags             []string
    DryRun           bool

    // Artifact configuration
    ArtifactsDir      string
    CaptureFullOutput bool
    MaxOutputBytes    int
    MaxOutputLines    int
}

type StepValidationError

StepValidationError represents step parameter validation failure during execution

type StepValidationError struct {
    Field   string
    Message string
}

func (*StepValidationError) Error

func (e *StepValidationError) Error() string

Generated by gomarkdoc