- ProcessTask injects $WORK_DIR only when script template uses it - File/directory type params: resolves file_id to filename before rendering - ValidateParams validates file/directory params as valid int64 file IDs - RenderScript no longer shell-escapes file/directory type values - Log rendered script before submitting to Slurm for debugging Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
126 lines
3.7 KiB
Go
126 lines
3.7 KiB
Go
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gcy_hpc_server/internal/model"
|
|
)
|
|
|
|
var paramNameRegex = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
|
|
|
// ValidateParams checks that all required parameters are present and values match their types.
|
|
// Parameters not in the schema are silently ignored.
|
|
func ValidateParams(params []model.ParameterSchema, values map[string]string) error {
|
|
var errs []string
|
|
|
|
for _, p := range params {
|
|
if !paramNameRegex.MatchString(p.Name) {
|
|
errs = append(errs, fmt.Sprintf("invalid parameter name %q: must match ^[A-Za-z_][A-Za-z0-9_]*$", p.Name))
|
|
continue
|
|
}
|
|
|
|
val, ok := values[p.Name]
|
|
|
|
if p.Required && !ok {
|
|
errs = append(errs, fmt.Sprintf("required parameter %q is missing", p.Name))
|
|
continue
|
|
}
|
|
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
switch p.Type {
|
|
case model.ParamTypeInteger:
|
|
if _, err := strconv.Atoi(val); err != nil {
|
|
errs = append(errs, fmt.Sprintf("parameter %q must be an integer, got %q", p.Name, val))
|
|
}
|
|
case model.ParamTypeBoolean:
|
|
if val != "true" && val != "false" && val != "1" && val != "0" {
|
|
errs = append(errs, fmt.Sprintf("parameter %q must be a boolean (true/false/1/0), got %q", p.Name, val))
|
|
}
|
|
case model.ParamTypeEnum:
|
|
if len(p.Options) > 0 {
|
|
found := false
|
|
for _, opt := range p.Options {
|
|
if val == opt {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
errs = append(errs, fmt.Sprintf("parameter %q must be one of %v, got %q", p.Name, p.Options, val))
|
|
}
|
|
}
|
|
case model.ParamTypeFile, model.ParamTypeDirectory:
|
|
if _, err := strconv.ParseInt(val, 10, 64); err != nil {
|
|
errs = append(errs, fmt.Sprintf("parameter %q must be a valid file ID (integer), got %q", p.Name, val))
|
|
}
|
|
case model.ParamTypeString:
|
|
}
|
|
}
|
|
|
|
if len(errs) > 0 {
|
|
return fmt.Errorf("parameter validation failed: %s", strings.Join(errs, "; "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RenderScript replaces $PARAM tokens in the template with user-provided values.
|
|
// Only tokens defined in the schema are replaced. Replacement is done longest-name-first
|
|
// to avoid partial matches (e.g., $JOB_NAME before $JOB).
|
|
// String, integer, boolean, and enum values are shell-escaped using single-quote wrapping.
|
|
// File and directory values (including WORK_DIR) are inserted raw (no escaping) because
|
|
// they are paths used in SBATCH directives and command arguments.
|
|
func RenderScript(template string, params []model.ParameterSchema, values map[string]string) string {
|
|
sorted := make([]model.ParameterSchema, len(params))
|
|
copy(sorted, params)
|
|
sort.Slice(sorted, func(i, j int) bool {
|
|
return len(sorted[i].Name) > len(sorted[j].Name)
|
|
})
|
|
|
|
// Add virtual "WORK_DIR" entry so $WORK_DIR is replaced without escaping.
|
|
sorted = append(sorted, model.ParameterSchema{Name: "WORK_DIR", Type: model.ParamTypeFile})
|
|
|
|
result := template
|
|
for _, p := range sorted {
|
|
val, ok := values[p.Name]
|
|
if !ok {
|
|
if p.Default != "" {
|
|
val = p.Default
|
|
} else {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if p.Type == model.ParamTypeFile || p.Type == model.ParamTypeDirectory {
|
|
result = strings.ReplaceAll(result, "$"+p.Name, val)
|
|
} else {
|
|
escaped := "'" + strings.ReplaceAll(val, "'", "'\\''") + "'"
|
|
result = strings.ReplaceAll(result, "$"+p.Name, escaped)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// SanitizeDirName sanitizes a directory name.
|
|
func SanitizeDirName(name string) string {
|
|
replacer := strings.NewReplacer(" ", "_", "/", "_", "\\", "_", ":", "_", "*", "_", "?", "_", "\"", "_", "<", "_", ">", "_", "|", "_")
|
|
return replacer.Replace(name)
|
|
}
|
|
|
|
// RandomSuffix generates a random suffix of length n.
|
|
func RandomSuffix(n int) string {
|
|
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
|
|
b := make([]byte, n)
|
|
for i := range b {
|
|
b[i] = charset[rand.Intn(len(charset))]
|
|
}
|
|
return string(b)
|
|
}
|