New package internal/jobdef mirrors the schema package pattern: - Load/LoadAll/Validate for YAML job definitions - Supports trigger types: revision_created, bom_changed, manual, schedule - Supports scope types: item, assembly, project - Supports compute types: validate, rebuild, diff, export, custom - Defaults: timeout=600s, max_retries=1, priority=100 Example definitions in jobdefs/: - assembly-validate.yaml: incremental validation on revision_created - part-export-step.yaml: STEP export on manual trigger 11 unit tests, all passing.
167 lines
4.1 KiB
Go
167 lines
4.1 KiB
Go
// Package jobdef handles YAML job definition parsing and validation.
|
|
package jobdef
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// Definition represents a compute job definition loaded from YAML.
|
|
type Definition struct {
|
|
Name string `yaml:"name" json:"name"`
|
|
Version int `yaml:"version" json:"version"`
|
|
Description string `yaml:"description" json:"description"`
|
|
Trigger TriggerConfig `yaml:"trigger" json:"trigger"`
|
|
Scope ScopeConfig `yaml:"scope" json:"scope"`
|
|
Compute ComputeConfig `yaml:"compute" json:"compute"`
|
|
Runner RunnerConfig `yaml:"runner" json:"runner"`
|
|
Timeout int `yaml:"timeout" json:"timeout"`
|
|
MaxRetries int `yaml:"max_retries" json:"max_retries"`
|
|
Priority int `yaml:"priority" json:"priority"`
|
|
}
|
|
|
|
// TriggerConfig describes when a job is created.
|
|
type TriggerConfig struct {
|
|
Type string `yaml:"type" json:"type"`
|
|
Filter map[string]string `yaml:"filter,omitempty" json:"filter,omitempty"`
|
|
}
|
|
|
|
// ScopeConfig describes what a job operates on.
|
|
type ScopeConfig struct {
|
|
Type string `yaml:"type" json:"type"`
|
|
}
|
|
|
|
// ComputeConfig describes the computation to perform.
|
|
type ComputeConfig struct {
|
|
Type string `yaml:"type" json:"type"`
|
|
Command string `yaml:"command" json:"command"`
|
|
Args map[string]any `yaml:"args,omitempty" json:"args,omitempty"`
|
|
}
|
|
|
|
// RunnerConfig describes runner requirements.
|
|
type RunnerConfig struct {
|
|
Tags []string `yaml:"tags" json:"tags"`
|
|
}
|
|
|
|
// DefinitionFile wraps a definition for YAML parsing.
|
|
type DefinitionFile struct {
|
|
Job Definition `yaml:"job"`
|
|
}
|
|
|
|
var validTriggerTypes = map[string]bool{
|
|
"revision_created": true,
|
|
"bom_changed": true,
|
|
"manual": true,
|
|
"schedule": true,
|
|
}
|
|
|
|
var validScopeTypes = map[string]bool{
|
|
"item": true,
|
|
"assembly": true,
|
|
"project": true,
|
|
}
|
|
|
|
var validComputeTypes = map[string]bool{
|
|
"validate": true,
|
|
"rebuild": true,
|
|
"diff": true,
|
|
"export": true,
|
|
"custom": true,
|
|
}
|
|
|
|
// Load reads a job definition from a YAML file.
|
|
func Load(path string) (*Definition, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading job definition file: %w", err)
|
|
}
|
|
|
|
var df DefinitionFile
|
|
if err := yaml.Unmarshal(data, &df); err != nil {
|
|
return nil, fmt.Errorf("parsing job definition YAML: %w", err)
|
|
}
|
|
|
|
def := &df.Job
|
|
|
|
// Apply defaults
|
|
if def.Timeout <= 0 {
|
|
def.Timeout = 600
|
|
}
|
|
if def.MaxRetries <= 0 {
|
|
def.MaxRetries = 1
|
|
}
|
|
if def.Priority <= 0 {
|
|
def.Priority = 100
|
|
}
|
|
if def.Version <= 0 {
|
|
def.Version = 1
|
|
}
|
|
|
|
if err := def.Validate(); err != nil {
|
|
return nil, fmt.Errorf("validating %s: %w", path, err)
|
|
}
|
|
|
|
return def, nil
|
|
}
|
|
|
|
// LoadAll reads all job definitions from a directory.
|
|
func LoadAll(dir string) (map[string]*Definition, error) {
|
|
defs := make(map[string]*Definition)
|
|
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading job definitions directory: %w", err)
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
continue
|
|
}
|
|
if !strings.HasSuffix(entry.Name(), ".yaml") && !strings.HasSuffix(entry.Name(), ".yml") {
|
|
continue
|
|
}
|
|
|
|
path := filepath.Join(dir, entry.Name())
|
|
def, err := Load(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("loading %s: %w", entry.Name(), err)
|
|
}
|
|
defs[def.Name] = def
|
|
}
|
|
|
|
return defs, nil
|
|
}
|
|
|
|
// Validate checks that the definition is well-formed.
|
|
func (d *Definition) Validate() error {
|
|
if d.Name == "" {
|
|
return fmt.Errorf("job definition name is required")
|
|
}
|
|
if d.Trigger.Type == "" {
|
|
return fmt.Errorf("trigger type is required")
|
|
}
|
|
if !validTriggerTypes[d.Trigger.Type] {
|
|
return fmt.Errorf("invalid trigger type %q", d.Trigger.Type)
|
|
}
|
|
if d.Scope.Type == "" {
|
|
return fmt.Errorf("scope type is required")
|
|
}
|
|
if !validScopeTypes[d.Scope.Type] {
|
|
return fmt.Errorf("invalid scope type %q", d.Scope.Type)
|
|
}
|
|
if d.Compute.Type == "" {
|
|
return fmt.Errorf("compute type is required")
|
|
}
|
|
if !validComputeTypes[d.Compute.Type] {
|
|
return fmt.Errorf("invalid compute type %q", d.Compute.Type)
|
|
}
|
|
if d.Compute.Command == "" {
|
|
return fmt.Errorf("compute command is required")
|
|
}
|
|
return nil
|
|
}
|