Skip to content

Commit de950a7

Browse files
RenaudWasTakenanshulpundir
authored andcommitted
Generic resource cli (#2347)
* Generic resource now uses csv format Signed-off-by: Renaud Gaubert <[email protected]> * Tested new input format Signed-off-by: Renaud Gaubert <[email protected]> * Updated Generic resource design to use new CLI format Signed-off-by: Renaud Gaubert <[email protected]> * Updated swarmctl and swarmd to use the new Generic resource CLI format Signed-off-by: Renaud Gaubert <[email protected]>
1 parent 312be59 commit de950a7

File tree

6 files changed

+97
-24
lines changed

6 files changed

+97
-24
lines changed

api/genericresource/parse.go

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package genericresource
22

33
import (
4+
"encoding/csv"
45
"fmt"
56
"strconv"
67
"strings"
@@ -12,36 +13,99 @@ func newParseError(format string, args ...interface{}) error {
1213
return fmt.Errorf("could not parse GenericResource: "+format, args...)
1314
}
1415

15-
// Parse parses the GenericResource resources given by the arguments
16-
func Parse(cmd string) ([]*api.GenericResource, error) {
17-
var rs []*api.GenericResource
16+
// discreteResourceVal returns an int64 if the string is a discreteResource
17+
// and an error if it isn't
18+
func discreteResourceVal(res string) (int64, error) {
19+
return strconv.ParseInt(res, 10, 64)
20+
}
21+
22+
// allNamedResources returns true if the array of resources are all namedResources
23+
// e.g: res = [red, orange, green]
24+
func allNamedResources(res []string) bool {
25+
for _, v := range res {
26+
if _, err := discreteResourceVal(v); err == nil {
27+
return false
28+
}
29+
}
30+
31+
return true
32+
}
33+
34+
// ParseCmd parses the Generic Resource command line argument
35+
// and returns a list of *api.GenericResource
36+
func ParseCmd(cmd string) ([]*api.GenericResource, error) {
37+
if strings.Contains(cmd, "\n") {
38+
return nil, newParseError("unexpected '\\n' character")
39+
}
40+
41+
r := csv.NewReader(strings.NewReader(cmd))
42+
records, err := r.ReadAll()
43+
44+
if err != nil {
45+
return nil, newParseError("%v", err)
46+
}
47+
48+
if len(records) != 1 {
49+
return nil, newParseError("found multiple records while parsing cmd %v", records)
50+
}
1851

19-
for _, term := range strings.Split(cmd, ";") {
52+
return Parse(records[0])
53+
}
54+
55+
// Parse parses a table of GenericResource resources
56+
func Parse(cmds []string) ([]*api.GenericResource, error) {
57+
tokens := make(map[string][]string)
58+
59+
for _, term := range cmds {
2060
kva := strings.Split(term, "=")
2161
if len(kva) != 2 {
22-
return nil, newParseError("incorrect term %s, missing '=' or malformed expr", term)
62+
return nil, newParseError("incorrect term %s, missing"+
63+
" '=' or malformed expression", term)
2364
}
2465

2566
key := strings.TrimSpace(kva[0])
2667
val := strings.TrimSpace(kva[1])
2768

28-
u, err := strconv.ParseInt(val, 10, 64)
29-
if err == nil {
69+
tokens[key] = append(tokens[key], val)
70+
}
71+
72+
var rs []*api.GenericResource
73+
for k, v := range tokens {
74+
if u, ok := isDiscreteResource(v); ok {
3075
if u < 0 {
31-
return nil, newParseError("cannot ask for negative resource %s", key)
76+
return nil, newParseError("cannot ask for"+
77+
" negative resource %s", k)
3278
}
33-
rs = append(rs, NewDiscrete(key, u))
79+
80+
rs = append(rs, NewDiscrete(k, u))
3481
continue
3582
}
3683

37-
if len(val) > 2 && val[0] == '{' && val[len(val)-1] == '}' {
38-
val = val[1 : len(val)-1]
39-
rs = append(rs, NewSet(key, strings.Split(val, ",")...)...)
84+
if allNamedResources(v) {
85+
rs = append(rs, NewSet(k, v...)...)
4086
continue
4187
}
4288

43-
return nil, newParseError("could not parse expression '%s'", term)
89+
return nil, newParseError("mixed discrete and named resources"+
90+
" in expression '%s=%s'", k, v)
4491
}
4592

4693
return rs, nil
4794
}
95+
96+
// isDiscreteResource returns true if the array of resources is a
97+
// Discrete Resource.
98+
// e.g: res = [1]
99+
func isDiscreteResource(values []string) (int64, bool) {
100+
if len(values) != 1 {
101+
return int64(0), false
102+
}
103+
104+
u, err := discreteResourceVal(values[0])
105+
if err != nil {
106+
return int64(0), false
107+
}
108+
109+
return u, true
110+
111+
}

api/genericresource/parse_test.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,26 @@ import (
77
)
88

99
func TestParseDiscrete(t *testing.T) {
10-
res, err := Parse("apple=3")
10+
res, err := ParseCmd("apple=3")
1111
assert.NoError(t, err)
1212
assert.Equal(t, len(res), 1)
1313

1414
apples := GetResource("apple", res)
1515
assert.Equal(t, len(apples), 1)
1616
assert.Equal(t, apples[0].GetDiscreteResourceSpec().Value, int64(3))
17+
18+
_, err = ParseCmd("apple=3\napple=4")
19+
assert.Error(t, err)
20+
21+
_, err = ParseCmd("apple=3,apple=4")
22+
assert.Error(t, err)
23+
24+
_, err = ParseCmd("apple=-3")
25+
assert.Error(t, err)
1726
}
1827

1928
func TestParseStr(t *testing.T) {
20-
res, err := Parse("orange={red,green,blue}")
29+
res, err := ParseCmd("orange=red,orange=green,orange=blue")
2130
assert.NoError(t, err)
2231
assert.Equal(t, len(res), 3)
2332

@@ -29,7 +38,7 @@ func TestParseStr(t *testing.T) {
2938
}
3039

3140
func TestParseDiscreteAndStr(t *testing.T) {
32-
res, err := Parse("orange={red,green,blue};apple=3")
41+
res, err := ParseCmd("orange=red,orange=green,orange=blue,apple=3")
3342
assert.NoError(t, err)
3443
assert.Equal(t, len(res), 4)
3544

cmd/swarmctl/service/flagparser/flags.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func AddServiceFlags(flags *pflag.FlagSet) {
3232
flags.String("memory-limit", "", "memory limit (e.g. 512m)")
3333
flags.String("cpu-reservation", "", "number of CPU cores reserved (e.g. 0.5)")
3434
flags.String("cpu-limit", "", "CPU cores limit (e.g. 0.5)")
35-
flags.String("generic-resources", "", "user defined resources request (e.g. gpu=3;fpga=1)")
35+
flags.String("generic-resources", "", "user defined resources request (e.g. gpu=3,fpga=1)")
3636

3737
flags.Uint64("update-parallelism", 0, "task update parallelism (0 = all at once)")
3838
flags.String("update-delay", "0s", "delay between task updates (0s = none)")

cmd/swarmctl/service/flagparser/resource.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func parseResource(flags *pflag.FlagSet, spec *api.ServiceSpec) error {
104104
if err != nil {
105105
return err
106106
}
107-
spec.Task.Resources.Reservations.Generic, err = genericresource.Parse(cmd)
107+
spec.Task.Resources.Reservations.Generic, err = genericresource.ParseCmd(cmd)
108108
if err != nil {
109109
return err
110110
}

cmd/swarmd/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ var (
168168
if err != nil {
169169
return err
170170
}
171-
resources, err = genericresource.Parse(genericResources)
171+
resources, err = genericresource.ParseCmd(genericResources)
172172
if err != nil {
173173
return err
174174
}
@@ -287,7 +287,7 @@ func init() {
287287
mainCmd.Flags().String("listen-debug", "", "Bind the Go debug server on the provided address")
288288
mainCmd.Flags().String("listen-metrics", "", "Listen address for metrics")
289289
mainCmd.Flags().String("join-addr", "", "Join cluster with a node at this address")
290-
mainCmd.Flags().String("generic-node-resources", "", "user defined resources (e.g. fpga=2;gpu={UUID1,UUID2,UUID3})")
290+
mainCmd.Flags().String("generic-node-resources", "", "user defined resources (e.g. fpga=2,gpu=UUID1,gpu=UUID2,gpu=UUID3)")
291291
mainCmd.Flags().Bool("force-new-cluster", false, "Force the creation of a new cluster from data directory")
292292
mainCmd.Flags().Uint32("heartbeat-tick", 1, "Defines the heartbeat interval (in seconds) for raft member health-check")
293293
mainCmd.Flags().Uint32("election-tick", 3, "Defines the amount of ticks (in seconds) needed without a Leader to trigger a new election")

design/generic_resources.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ of resources, ...) and should not be addressed in this PR.
9393
$ # Single resource
9494
$ swarmctl service create --name nginx --image nginx:latest --generic-resources "banana=2"
9595
$ # Multiple resources
96-
$ swarmctl service create --name nginx --image nginx:latest --generic-resources "banana=2;apple=3"
96+
$ swarmctl service create --name nginx --image nginx:latest --generic-resources "banana=2,apple=3"
9797
```
9898

9999
### Generic Resource advertising
@@ -103,7 +103,7 @@ It is the scheduler's job to decide which resource to assign and keep track of w
103103
owns which resource.
104104

105105
```
106-
$ swarmd -d $DIR --join-addr $IP --join-token $TOKEN --generic-node-resources "banana={blue,red,green};apple=8"
106+
$ swarmd -d $DIR --join-addr $IP --join-token $TOKEN --generic-node-resources "banana=blue,banana=red,banana=green,apple=8"
107107
```
108108

109109
### Generic Resource communication
@@ -142,7 +142,7 @@ Plugins:
142142
Volume : [local nvidia-docker]
143143
Engine Version : 1.13.1
144144

145-
$ swarmctl service create --name nginx --image nginx:latest --generic-resources "banana=2;apple=2"
145+
$ swarmctl service create --name nginx --image nginx:latest --generic-resources "banana=2,apple=2"
146146
$ swarmctl service inspect nginx
147147
ID : abxelhl822d8zyjqam3m3szb0
148148
Name : nginx
@@ -161,7 +161,7 @@ Task ID Service Slot Image Desired State
161161

162162
$ # ssh to the node
163163
$ docker inspect $CONTAINER_ID --format '{{.Config.Env}}' | tr -s ' ' '\n'
164-
[DOCKER_RESOURCE_BANANA=2
164+
[DOCKER_RESOURCE_BANANA=red,blue
165165
DOCKER_RESOURCE_APPLE=2
166166
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
167167
NGINX_VERSION=1.13.0-1~stretch

0 commit comments

Comments
 (0)