mirror of
https://github.com/dagu-org/dagu.git
synced 2025-12-28 06:34:22 +00:00
* **Behavior Changes** * Removed explicit no-queue and disable-max-active-runs options; start/retry flows simplified to default local execution and streamlined retry semantics. * **New Features** * Singleton mode now returns clear HTTP 409 conflicts when a singleton DAG is already running or queued. * Added top-level run Error field and an API to record early failures for quicker failure visibility. * **Bug Fixes** * Improved process acquisition and restart/retry error handling; tests updated to reflect local execution behavior.
130 lines
3.5 KiB
Go
130 lines
3.5 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/dagu-org/dagu/internal/common/fileutil"
|
|
"github.com/dagu-org/dagu/internal/common/stringutil"
|
|
"github.com/dagu-org/dagu/internal/core"
|
|
"github.com/dagu-org/dagu/internal/core/spec"
|
|
"github.com/goccy/go-yaml"
|
|
)
|
|
|
|
// ExecOptions captures the inline configuration for building an ad-hoc DAG.
|
|
type ExecOptions struct {
|
|
Name string
|
|
CommandArgs []string
|
|
ShellOverride string
|
|
WorkingDir string
|
|
Env []string
|
|
DotenvFiles []string
|
|
BaseConfig string
|
|
WorkerLabels map[string]string
|
|
}
|
|
|
|
type execSpec struct {
|
|
Name string `yaml:"name,omitempty"`
|
|
Type string `yaml:"type,omitempty"`
|
|
WorkingDir string `yaml:"workingDir,omitempty"`
|
|
Env []string `yaml:"env,omitempty"`
|
|
Dotenv []string `yaml:"dotenv,omitempty"`
|
|
WorkerSelector map[string]string `yaml:"workerSelector,omitempty"`
|
|
Steps []execStep `yaml:"steps"`
|
|
}
|
|
|
|
type execStep struct {
|
|
Name string `yaml:"name"`
|
|
Command []string `yaml:"command,omitempty"`
|
|
Shell string `yaml:"shell,omitempty"`
|
|
}
|
|
|
|
func buildExecDAG(ctx *Context, opts ExecOptions) (*core.DAG, string, error) {
|
|
if len(opts.CommandArgs) == 0 {
|
|
return nil, "", fmt.Errorf("command is required to build exec DAG")
|
|
}
|
|
|
|
name := strings.TrimSpace(opts.Name)
|
|
if name == "" {
|
|
name = defaultExecName(opts.CommandArgs[0])
|
|
}
|
|
if err := core.ValidateDAGName(name); err != nil {
|
|
return nil, "", fmt.Errorf("invalid DAG name: %w", err)
|
|
}
|
|
|
|
specDoc := execSpec{
|
|
Name: name,
|
|
Type: core.TypeChain,
|
|
WorkingDir: opts.WorkingDir,
|
|
Env: opts.Env,
|
|
Dotenv: opts.DotenvFiles,
|
|
WorkerSelector: opts.WorkerLabels,
|
|
Steps: []execStep{
|
|
{
|
|
Name: defaultStepName,
|
|
Command: opts.CommandArgs,
|
|
Shell: opts.ShellOverride,
|
|
},
|
|
},
|
|
}
|
|
|
|
specYAML, err := yaml.Marshal(specDoc)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("failed to marshal generated DAG spec: %w", err)
|
|
}
|
|
|
|
tempFile, err := os.CreateTemp("", fmt.Sprintf("dagu-exec-%s-*.yaml", fileutil.SafeName(name)))
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("failed to create temporary DAG file: %w", err)
|
|
}
|
|
tempPath := tempFile.Name()
|
|
if _, err = tempFile.Write(specYAML); err != nil {
|
|
_ = tempFile.Close()
|
|
_ = os.Remove(tempPath)
|
|
return nil, "", fmt.Errorf("failed to write temporary DAG file: %w", err)
|
|
}
|
|
if err := tempFile.Close(); err != nil {
|
|
_ = os.Remove(tempPath)
|
|
return nil, "", fmt.Errorf("failed to close temporary DAG file: %w", err)
|
|
}
|
|
defer func() {
|
|
_ = os.Remove(tempPath)
|
|
}()
|
|
|
|
var loadOpts []spec.LoadOption
|
|
loadOpts = append(loadOpts, spec.WithName(name))
|
|
|
|
if base := ctx.Config.Paths.BaseConfig; base != "" {
|
|
loadOpts = append(loadOpts, spec.WithBaseConfig(base))
|
|
}
|
|
if opts.BaseConfig != "" {
|
|
loadOpts = append(loadOpts, spec.WithBaseConfig(opts.BaseConfig))
|
|
}
|
|
|
|
dag, err := spec.Load(ctx, tempPath, loadOpts...)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("failed to load generated DAG: %w", err)
|
|
}
|
|
|
|
dag.Name = name
|
|
dag.WorkingDir = opts.WorkingDir
|
|
if len(opts.WorkerLabels) > 0 {
|
|
dag.WorkerSelector = opts.WorkerLabels
|
|
}
|
|
dag.MaxActiveRuns = -1
|
|
dag.Location = ""
|
|
|
|
return dag, string(specYAML), nil
|
|
}
|
|
|
|
func defaultExecName(command string) string {
|
|
base := fileutil.SafeName(filepath.Base(command))
|
|
if base == "" {
|
|
base = "command"
|
|
}
|
|
name := "exec-" + base
|
|
return stringutil.TruncString(name, core.DAGNameMaxLen)
|
|
}
|