Skip to content

Commit 90dd999

Browse files
authored
feat: add nextgen jobspec (#10)
* feat: add nextgen jobspec Signed-off-by: vsoch <vsoch@users.noreply.github.com>
1 parent 886aab9 commit 90dd999

File tree

9 files changed

+515
-3
lines changed

9 files changed

+515
-3
lines changed

Makefile

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ COMMONENVVAR=GOOS=$(shell uname -s | tr A-Z a-z)
33
RELEASE_VERSION?=v$(shell date +%Y%m%d)-$(shell git describe --tags --match "v*")
44

55
.PHONY: all
6-
all: example1 example2 example3 example4 example5 example6 createnew exp1 exp2
6+
all: example1 example2 example3 example4 example5 example6 createnew exp1 exp2 ng1
77

88
.PHONY: build
99
build:
1010
go mod tidy
1111
mkdir -p ./examples/v1/bin
1212
mkdir -p ./examples/experimental/bin
13+
mkdir -p ./examples/nextgen/v1/bin
1314

1415
# Build examples
1516
.PHONY: createnew
@@ -48,8 +49,13 @@ exp1: build
4849
exp2: build
4950
$(COMMONENVVAR) $(BUILDENVVAR) go build -ldflags '-w' -o ./examples/experimental/bin/example2 examples/experimental/example2/example.go
5051

52+
.PHONY: ng1
53+
ng1: build
54+
$(COMMONENVVAR) $(BUILDENVVAR) go build -ldflags '-w' -o ./examples/nextgen/v1/bin/example1 examples/nextgen/v1/example1/example.go
55+
56+
5157
.PHONY: test
52-
test: all
58+
test: build all
5359
./examples/v1/bin/example1
5460
./examples/v1/bin/example2
5561
./examples/v1/bin/example3
@@ -59,6 +65,7 @@ test: all
5965
./examples/v1/bin/createnew
6066
./examples/experimental/bin/example1
6167
./examples/experimental/bin/example2
68+
./examples/nextgen/v1/bin/example1
6269

6370
.PHONY: clean
6471
clean:

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
# Flux Jobspec (Go)
22

3-
This is a simple library that provides go structures for the Flux Framework [Jobspec](https://flux-framework.readthedocs.io/projects/flux-rfc/en/latest/spec_25.html) for use in other projects.
3+
This is a simple library that provides go structures for:
4+
5+
- the Flux Framework [Jobspec](https://flux-framework.readthedocs.io/projects/flux-rfc/en/latest/spec_25.html) (package [jobspec](pkg/jobspec))
6+
- the [Next Generation Jobspec](https://compspec.github.io/jobspec) (package [nextgen](pkg/nextgen/))
7+
8+
Note for nextgen, since Go is more strict with typing, we accept a parsed JobSpec, meaning that all resources have been defined in the top level named section,
9+
and are referenced by name in tasks. We will start assuming that a request for the resource groups should be satisfied within the same cluster, and each is a separate
10+
match request.
411

512
## Usage
613

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"os"
7+
8+
v1 "github.com/compspec/jobspec-go/pkg/nextgen/v1"
9+
)
10+
11+
func main() {
12+
fmt.Println("This example reads, parses, and validates a Jobspec")
13+
14+
// Assumes running from the root
15+
fileName := flag.String("json", "examples/nextgen/v1/example1/jobspec.yaml", "yaml file")
16+
flag.Parse()
17+
18+
yamlFile := *fileName
19+
if yamlFile == "" {
20+
flag.Usage()
21+
os.Exit(0)
22+
}
23+
js, err := v1.LoadJobspecYaml(yamlFile)
24+
if err != nil {
25+
fmt.Printf("error reading %s:%s\n", yamlFile, err)
26+
os.Exit(1)
27+
}
28+
29+
// Validate the jobspec
30+
valid, err := js.Validate()
31+
if !valid || err != nil {
32+
fmt.Printf("schema is not valid:%s\n", err)
33+
os.Exit(1)
34+
} else {
35+
fmt.Println("schema is valid")
36+
}
37+
38+
out, err := js.JobspecToYaml()
39+
if err != nil {
40+
fmt.Printf("error marshalling %s:%s\n", yamlFile, err)
41+
os.Exit(1)
42+
}
43+
fmt.Println(string(out))
44+
45+
// One example of json
46+
out, err = js.JobspecToJson()
47+
if err != nil {
48+
fmt.Printf("error marshalling %s:%s\n", yamlFile, err)
49+
os.Exit(1)
50+
}
51+
fmt.Println(string(out))
52+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
version: 1
2+
3+
# Resources can be used with tasks or task groups
4+
# They are named so can be referenced in multiple places,
5+
# and used with AND and OR
6+
resources:
7+
mini-mummi:
8+
count: 8
9+
type: node
10+
with:
11+
- type: gpu
12+
count: 2
13+
- type: cores
14+
count: 4
15+
16+
mummi-gpu:
17+
count: 2
18+
type: node
19+
with:
20+
- type: gpu
21+
count: 1
22+
23+
mummi-cpu:
24+
count: 2
25+
type: node
26+
with:
27+
- type: cores
28+
count: 4
29+
30+
31+
tasks:
32+
- name: task-1
33+
resources: common
34+
command:
35+
- bash
36+
- -c
37+
- "echo Starting task 1; sleep 3; echo Finishing task 1"
38+
39+
# flux batch...
40+
- group: group-1
41+
42+
# A group is a "flux batch"
43+
groups:
44+
- name: mini-mummi
45+
resources: mini-mummi
46+
tasks:
47+
48+
# flux batch
49+
- group: train
50+
51+
# flux submit to train job
52+
- name: test
53+
replicas: 20
54+
resources: mummi-cpu
55+
command:
56+
- bash
57+
- -c
58+
- echo Running machine learning test
59+
60+
# flux batch from mini-mummi group
61+
- name: train
62+
resources: "mummi-gpu|mummi-cpu"
63+
tasks:
64+
65+
# If a task doesn't have resources, it inherits parent group (uses all)
66+
- name: train
67+
command:
68+
- bash
69+
- -c
70+
- echo running machine learning train

pkg/nextgen/v1/convert.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package v1
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
// NewSimpleJobSpec generates a simple jobspec for nodes, command, tasks, and (optionally) a name
9+
func NewSimpleJobspec(name, command string, nodes, tasks int32) (*Jobspec, error) {
10+
11+
// If no name provided for the slot, use the first
12+
// work of the command
13+
if name == "" {
14+
parts := strings.Split(command, " ")
15+
name = strings.ToLower(parts[0])
16+
}
17+
if nodes < 1 {
18+
return nil, fmt.Errorf("nodes for the job must be >= 1")
19+
}
20+
if command == "" {
21+
return nil, fmt.Errorf("a command must be provided")
22+
}
23+
24+
// The node resource is what we are asking for
25+
nodeResource := Resource{
26+
Type: "node",
27+
Count: nodes,
28+
}
29+
30+
// The slot is where we are doing an assessment for scheduling
31+
slot := Resource{
32+
Type: "slot",
33+
Count: int32(1),
34+
Label: name,
35+
}
36+
37+
// If tasks are defined, this is total tasks across the nodes
38+
// We add to the slot
39+
if tasks != 0 {
40+
taskResource := Resource{
41+
Type: "core",
42+
Count: tasks,
43+
}
44+
slot.With = []Resource{taskResource}
45+
}
46+
47+
// And then the entire resource spec is added to the top level node resource
48+
nodeResource.With = []Resource{slot}
49+
50+
// Resource name matches resources to named set
51+
resourceName := "task-resources"
52+
53+
// Tasks reference the slot and command
54+
// Note: if we need better split can use "github.com/google/shlex"
55+
cmd := strings.Split(command, " ")
56+
taskResource := Task{
57+
Command: cmd,
58+
Replicas: 1,
59+
Resources: resourceName,
60+
}
61+
tasklist := []Task{taskResource}
62+
63+
return &Jobspec{
64+
Version: jobspecVersion,
65+
Tasks: tasklist,
66+
Resources: Resources{resourceName: nodeResource},
67+
}, nil
68+
}

pkg/nextgen/v1/jobspec.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package v1
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
8+
"sigs.k8s.io/yaml"
9+
10+
"github.com/compspec/jobspec-go/pkg/schema"
11+
)
12+
13+
// LoadJobspecYaml loads a jobspec from a yaml file path
14+
func LoadJobspecYaml(yamlFile string) (*Jobspec, error) {
15+
js := Jobspec{}
16+
file, err := os.ReadFile(yamlFile)
17+
if err != nil {
18+
return &js, err
19+
}
20+
21+
err = yaml.Unmarshal([]byte(file), &js)
22+
if err != nil {
23+
return &js, err
24+
}
25+
return &js, nil
26+
}
27+
28+
// JobspectoYaml convets back to yaml (as string)
29+
func (js *Jobspec) JobspecToYaml() (string, error) {
30+
out, err := yaml.Marshal(js)
31+
if err != nil {
32+
return "", err
33+
}
34+
return string(out), nil
35+
}
36+
37+
// GetResources to get resource groups across the jobspec
38+
// this is intended for graph scheduling
39+
func (js *Jobspec) GetResources(data []byte) ([]Resource, error) {
40+
41+
// We assume every discovered resource is a unique satisfy
42+
resources := []Resource{}
43+
44+
// Make sure all task and group resources are known
45+
for _, task := range js.Tasks {
46+
r, err := js.getResources(task)
47+
if err != nil {
48+
return resources, err
49+
}
50+
resources = append(resources, r)
51+
}
52+
for _, group := range js.Groups {
53+
r, err := js.getResources(group)
54+
if err != nil {
55+
return resources, err
56+
}
57+
resources = append(resources, r)
58+
}
59+
return resources, nil
60+
}
61+
62+
// getResources unwraps resources. If there is a named string, we assume
63+
// in reference to a named resource group. We will need a strategy to combine
64+
// these intelligently when we ask for a match - right now just assuming
65+
// separate groups
66+
func (js *Jobspec) getResources(resources interface{}) (Resource, error) {
67+
resource := Resource{}
68+
switch v := resources.(type) {
69+
case string:
70+
resourceKey := resources.(string)
71+
spec, ok := js.Resources[resourceKey]
72+
if !ok {
73+
return resource, fmt.Errorf("task is missing resource")
74+
}
75+
return spec, nil
76+
case Resource:
77+
return resources.(Resource), nil
78+
default:
79+
return resource, fmt.Errorf("type %s is unknown", v)
80+
}
81+
}
82+
83+
// JobspectoJson convets back to json string
84+
func (js *Jobspec) JobspecToJson() (string, error) {
85+
out, err := json.MarshalIndent(js, "", " ")
86+
if err != nil {
87+
return "", err
88+
}
89+
return string(out), nil
90+
}
91+
92+
// Validate converts to bytes and validate with jsonschema
93+
func (js *Jobspec) Validate() (bool, error) {
94+
95+
// Get back into bytes form
96+
out, err := yaml.Marshal(js)
97+
if err != nil {
98+
return false, err
99+
}
100+
// Validate the jobspec
101+
return schema.Validate(out, schema.SchemaUrl, Schema)
102+
103+
}
104+
105+
// Helper function to get a job name, derived from the command
106+
func (js *Jobspec) GetJobName() string {
107+
108+
// Generic name to fall back tp
109+
name := "app"
110+
if js.Name != "" {
111+
name = js.Name
112+
}
113+
return name
114+
}

pkg/nextgen/v1/schema.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package v1
2+
3+
import (
4+
_ "embed"
5+
)
6+
7+
//go:embed schema.json
8+
var Schema string

0 commit comments

Comments
 (0)