c3dfc9315f
Pure functions for parsing dagu-compatible YAML, validating DAG structure, topological sorting with parallel levels (Kahn's algorithm), and env variable resolution. Also adds cron_match for schedule matching. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
282 lines
7.6 KiB
Go
282 lines
7.6 KiB
Go
package core
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// rawDagStep is the loosely-typed intermediate representation of a DAG step.
|
|
type rawDagStep struct {
|
|
Name string `yaml:"name"`
|
|
ID string `yaml:"id"`
|
|
Description string `yaml:"description"`
|
|
Command string `yaml:"command"`
|
|
Script string `yaml:"script"`
|
|
Args []string `yaml:"args"`
|
|
Shell string `yaml:"shell"`
|
|
Dir string `yaml:"dir"`
|
|
WorkingDir string `yaml:"working_dir"`
|
|
Depends []string `yaml:"depends"`
|
|
Env interface{} `yaml:"env"`
|
|
ContinueOn rawDagContinueOn `yaml:"continue_on"`
|
|
RetryPolicy rawDagRetryPolicy `yaml:"retry_policy"`
|
|
TimeoutSec int `yaml:"timeout_sec"`
|
|
Output string `yaml:"output"`
|
|
Tags []string `yaml:"tags"`
|
|
}
|
|
|
|
type rawDagContinueOn struct {
|
|
Failure bool `yaml:"failure"`
|
|
Skipped bool `yaml:"skipped"`
|
|
}
|
|
|
|
type rawDagRetryPolicy struct {
|
|
Limit int `yaml:"limit"`
|
|
IntervalSec int `yaml:"interval_sec"`
|
|
}
|
|
|
|
// rawDagHandlers handles both handler_on and handlers aliases.
|
|
type rawDagHandlers struct {
|
|
Init interface{} `yaml:"init"`
|
|
Success interface{} `yaml:"success"`
|
|
Failure interface{} `yaml:"failure"`
|
|
Exit interface{} `yaml:"exit"`
|
|
}
|
|
|
|
// rawDag is the loosely-typed intermediate representation of a DAG YAML.
|
|
type rawDag struct {
|
|
Name string `yaml:"name"`
|
|
Description string `yaml:"description"`
|
|
Group string `yaml:"group"`
|
|
Type string `yaml:"type"`
|
|
WorkingDir string `yaml:"working_dir"`
|
|
Shell string `yaml:"shell"`
|
|
Env interface{} `yaml:"env"`
|
|
Schedule interface{} `yaml:"schedule"`
|
|
Steps []rawDagStep `yaml:"steps"`
|
|
HandlerOn *rawDagHandlers `yaml:"handler_on"`
|
|
Handlers *rawDagHandlers `yaml:"handlers"`
|
|
Tags []string `yaml:"tags"`
|
|
TimeoutSec int `yaml:"timeout_sec"`
|
|
}
|
|
|
|
// DagParse parses a YAML DAG definition compatible with Dagu format.
|
|
// Handles schedule as string or list, env as list of single-key maps,
|
|
// handler_on and handlers as aliases, and steps with command/script/depends.
|
|
func DagParse(data []byte) (DagDefinition, error) {
|
|
var raw rawDag
|
|
if err := yaml.Unmarshal(data, &raw); err != nil {
|
|
return DagDefinition{}, fmt.Errorf("dag_parse: yaml unmarshal: %w", err)
|
|
}
|
|
|
|
def := DagDefinition{
|
|
Name: raw.Name,
|
|
Description: raw.Description,
|
|
Group: raw.Group,
|
|
Type: raw.Type,
|
|
WorkingDir: raw.WorkingDir,
|
|
Shell: raw.Shell,
|
|
Tags: raw.Tags,
|
|
TimeoutSec: raw.TimeoutSec,
|
|
}
|
|
|
|
// Normalize env (list of single-key maps or plain map).
|
|
dagEnv, err := normalizeEnv(raw.Env)
|
|
if err != nil {
|
|
return DagDefinition{}, fmt.Errorf("dag_parse: env: %w", err)
|
|
}
|
|
def.Env = dagEnv
|
|
|
|
// Normalize schedule (string or list).
|
|
def.Schedule = normalizeSchedule(raw.Schedule)
|
|
|
|
// Normalize steps.
|
|
steps := make([]DagStep, 0, len(raw.Steps))
|
|
for i, rs := range raw.Steps {
|
|
step, err := normalizeStep(rs)
|
|
if err != nil {
|
|
return DagDefinition{}, fmt.Errorf("dag_parse: step[%d]: %w", i, err)
|
|
}
|
|
steps = append(steps, step)
|
|
}
|
|
def.Steps = steps
|
|
|
|
// Normalize handlers: handler_on takes precedence, fallback to handlers.
|
|
rawHandlers := raw.HandlerOn
|
|
if rawHandlers == nil {
|
|
rawHandlers = raw.Handlers
|
|
}
|
|
if rawHandlers != nil {
|
|
handlers, err := normalizeHandlers(rawHandlers)
|
|
if err != nil {
|
|
return DagDefinition{}, fmt.Errorf("dag_parse: handlers: %w", err)
|
|
}
|
|
def.HandlerOn = handlers
|
|
}
|
|
|
|
return def, nil
|
|
}
|
|
|
|
// normalizeSchedule converts schedule from string or []interface{} to []string.
|
|
func normalizeSchedule(v interface{}) []string {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
switch t := v.(type) {
|
|
case string:
|
|
s := strings.TrimSpace(t)
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
return []string{s}
|
|
case []interface{}:
|
|
result := make([]string, 0, len(t))
|
|
for _, item := range t {
|
|
if s, ok := item.(string); ok {
|
|
s = strings.TrimSpace(s)
|
|
if s != "" {
|
|
result = append(result, s)
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// normalizeEnv converts env from Dagu format (list of single-key maps) or plain map to map[string]string.
|
|
func normalizeEnv(v interface{}) (map[string]string, error) {
|
|
if v == nil {
|
|
return nil, nil
|
|
}
|
|
switch t := v.(type) {
|
|
case map[string]interface{}:
|
|
// Plain YAML map.
|
|
result := make(map[string]string, len(t))
|
|
for k, val := range t {
|
|
result[k] = fmt.Sprintf("%v", val)
|
|
}
|
|
return result, nil
|
|
case []interface{}:
|
|
// Dagu format: list of single-key maps [KEY: value, KEY2: value2].
|
|
result := make(map[string]string)
|
|
for _, item := range t {
|
|
m, ok := item.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
for k, val := range m {
|
|
result[k] = fmt.Sprintf("%v", val)
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
return nil, fmt.Errorf("unexpected env type %T", v)
|
|
}
|
|
|
|
// normalizeStep converts a rawDagStep to a DagStep.
|
|
func normalizeStep(rs rawDagStep) (DagStep, error) {
|
|
stepEnv, err := normalizeEnv(rs.Env)
|
|
if err != nil {
|
|
return DagStep{}, fmt.Errorf("env: %w", err)
|
|
}
|
|
|
|
// working_dir is an alias for dir at the step level.
|
|
dir := rs.Dir
|
|
if dir == "" {
|
|
dir = rs.WorkingDir
|
|
}
|
|
|
|
return DagStep{
|
|
Name: rs.Name,
|
|
ID: rs.ID,
|
|
Description: rs.Description,
|
|
Command: rs.Command,
|
|
Script: rs.Script,
|
|
Args: rs.Args,
|
|
Shell: rs.Shell,
|
|
Dir: dir,
|
|
Depends: rs.Depends,
|
|
Env: stepEnv,
|
|
ContinueOn: DagContinueOn{
|
|
Failure: rs.ContinueOn.Failure,
|
|
Skipped: rs.ContinueOn.Skipped,
|
|
},
|
|
RetryPolicy: DagRetryPolicy{
|
|
Limit: rs.RetryPolicy.Limit,
|
|
IntervalSec: rs.RetryPolicy.IntervalSec,
|
|
},
|
|
TimeoutSec: rs.TimeoutSec,
|
|
Output: rs.Output,
|
|
Tags: rs.Tags,
|
|
}, nil
|
|
}
|
|
|
|
// normalizeHandlers converts rawDagHandlers to DagHandlers.
|
|
// Each handler field can be a single step object or a list of steps.
|
|
func normalizeHandlers(rh *rawDagHandlers) (DagHandlers, error) {
|
|
var h DagHandlers
|
|
var err error
|
|
h.Init, err = normalizeHandlerField(rh.Init)
|
|
if err != nil {
|
|
return DagHandlers{}, fmt.Errorf("init: %w", err)
|
|
}
|
|
h.Success, err = normalizeHandlerField(rh.Success)
|
|
if err != nil {
|
|
return DagHandlers{}, fmt.Errorf("success: %w", err)
|
|
}
|
|
h.Failure, err = normalizeHandlerField(rh.Failure)
|
|
if err != nil {
|
|
return DagHandlers{}, fmt.Errorf("failure: %w", err)
|
|
}
|
|
h.Exit, err = normalizeHandlerField(rh.Exit)
|
|
if err != nil {
|
|
return DagHandlers{}, fmt.Errorf("exit: %w", err)
|
|
}
|
|
return h, nil
|
|
}
|
|
|
|
// normalizeHandlerField converts a handler field (single step or list) to []DagStep.
|
|
func normalizeHandlerField(v interface{}) ([]DagStep, error) {
|
|
if v == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
// Re-marshal and unmarshal to handle polymorphic types cleanly.
|
|
b, err := yaml.Marshal(v)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("re-marshal handler: %w", err)
|
|
}
|
|
|
|
// Try as a list first.
|
|
var rawList []rawDagStep
|
|
if err := yaml.Unmarshal(b, &rawList); err == nil && len(rawList) > 0 {
|
|
steps := make([]DagStep, 0, len(rawList))
|
|
for i, rs := range rawList {
|
|
step, err := normalizeStep(rs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("step[%d]: %w", i, err)
|
|
}
|
|
steps = append(steps, step)
|
|
}
|
|
return steps, nil
|
|
}
|
|
|
|
// Try as single step.
|
|
var rs rawDagStep
|
|
if err := yaml.Unmarshal(b, &rs); err != nil {
|
|
return nil, fmt.Errorf("unmarshal handler step: %w", err)
|
|
}
|
|
step, err := normalizeStep(rs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// If the step has no meaningful content, return nil.
|
|
if step.Name == "" && step.ID == "" && step.Command == "" && step.Script == "" {
|
|
return nil, nil
|
|
}
|
|
return []DagStep{step}, nil
|
|
}
|