From 19c69850b83a7ec788e8de881d1067bab03421bf Mon Sep 17 00:00:00 2001 From: stackedsax Date: Sat, 18 Jul 2026 09:35:18 -0700 Subject: [PATCH] Round-trip multi-role JobSets; consolidate k8s helpers Kueue and YuniKorn emit multi-role jobs as a JobSet (one replicatedJob per role), but the parsers only understood a single batch/v1 Job, so those jobs could not round-trip back to SPLAT. Add JobSet parsing: - internal/jobset.ToTasks inverts the emitters' replicatedJobs into SPLAT tasks (execution incl. inlined-script detection, resources, placement, replicas) plus shared job volumes and the max BackoffLimit. - The Kueue and YuniKorn parsers detect kind: JobSet and delegate; YuniKorn additionally recovers gang scheduling from the task-groups annotation. Tech debt folded in while here: - Move the duplicated tolerations helper and the Volcano parser's volume recovery into k8senc (Tolerations, VolumesFromPod, SortVolumes); Volcano and the JobSet parser now share them. - Trigger the dry-run workflow on parser/** and k8senc/** changes too, not just emitters. Tests: JobSet parse for Kueue (container + inlined-script roles) and YuniKorn (gang from task-groups), plus an emit->parse round-trip guarding against emitter/parser drift. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_016PnTz6Zxqa4jHocK8kCbyx --- .github/workflows/dryrun.yml | 2 + internal/jobset/tosplat.go | 92 +++++++++++++ internal/k8senc/k8senc.go | 60 +++++++++ internal/parser/armada/parser.go | 11 +- internal/parser/kueue/parser.go | 32 +++++ internal/parser/kueue/parser_test.go | 164 ++++++++++++++++++++++++ internal/parser/volcano/parser.go | 67 +--------- internal/parser/yunikorn/parser.go | 41 +++++- internal/parser/yunikorn/parser_test.go | 78 +++++++++++ 9 files changed, 471 insertions(+), 76 deletions(-) create mode 100644 internal/jobset/tosplat.go diff --git a/.github/workflows/dryrun.yml b/.github/workflows/dryrun.yml index 56803f1..ce31d57 100644 --- a/.github/workflows/dryrun.yml +++ b/.github/workflows/dryrun.yml @@ -8,7 +8,9 @@ on: pull_request: paths: - 'internal/emitter/**' + - 'internal/parser/**' - 'internal/splat/**' + - 'internal/k8senc/**' - 'internal/*/types.go' - 'scripts/dryrun/**' - '.github/workflows/dryrun.yml' diff --git a/internal/jobset/tosplat.go b/internal/jobset/tosplat.go new file mode 100644 index 0000000..d6ab688 --- /dev/null +++ b/internal/jobset/tosplat.go @@ -0,0 +1,92 @@ +package jobset + +import ( + corev1 "k8s.io/api/core/v1" + + "github.com/InsightSoftmax/BAMMM/internal/k8senc" + "github.com/InsightSoftmax/BAMMM/internal/splat" +) + +// DefaultImage is the placeholder image the Kueue/YuniKorn JobSet emitters use +// for script and executable tasks that carry no source container. The parser +// uses it to recognize that such a container is really an inlined script. +const DefaultImage = "ubuntu:22.04" + +// ToTasks converts a JobSet's replicatedJobs into SPLAT tasks, plus the shared +// job-level volumes and the maximum in-pod retry (BackoffLimit) observed. It is +// the inverse of the Kueue and YuniKorn JobSet emitters; scheduler-specific +// metadata (queue labels, gang annotations) stays with the caller. +func ToTasks(js *JobSet) (tasks []splat.Task, volumes []splat.Volume, maxRetries int) { + seen := map[string]splat.Volume{} + for i := range js.Spec.ReplicatedJobs { + rj := &js.Spec.ReplicatedJobs[i] + spec := &rj.Template.Spec + if bl := spec.BackoffLimit; bl != nil && int(*bl) > maxRetries { + maxRetries = int(*bl) + } + + task := splat.Task{Name: rj.Name, Replicas: replicasOf(spec.Parallelism, spec.Completions)} + pod := &spec.Template.Spec + if len(pod.Containers) > 0 { + c := pod.Containers[0] + task.Resources = k8senc.ResourcesFromContainer(&c) + task.Execution = executionOf(&c) + k8senc.VolumesFromPod(&c, pod, seen) + } + task.Placement = placementOf(pod) + tasks = append(tasks, task) + } + return tasks, k8senc.SortVolumes(seen), maxRetries +} + +// replicasOf recovers a task's replica count from the wrapped Job's parallelism +// (the emitters set parallelism/completions only when replicas exceed 1). +func replicasOf(parallelism, completions *int32) int { + switch { + case parallelism != nil && *parallelism > 0: + return int(*parallelism) + case completions != nil && *completions > 0: + return int(*completions) + default: + return 1 + } +} + +// executionOf inverts the emitters' container: an inlined "/bin/bash -c