dagu/internal/cmd/exec_spec.go
Yota Hamada f3d4577e42
fix(cmd): refactor start command not to prevent DAG from running unexpectedly (#1497)
* **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.
2025-12-21 18:42:34 +09:00

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)
}