Skip to content

Commit 97b148b

Browse files
authored
Merge pull request #429 from RenaudWasTaken/generic-resource
Integrated Generic resource in service create
2 parents e004dc0 + 1ff73f8 commit 97b148b

File tree

25 files changed

+1186
-161
lines changed

25 files changed

+1186
-161
lines changed

cli/command/service/create.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/docker/cli/cli"
77
"github.com/docker/cli/cli/command"
8+
cliopts "github.com/docker/cli/opts"
89
"github.com/docker/docker/api/types"
910
"github.com/docker/docker/api/types/versions"
1011
"github.com/spf13/cobra"
@@ -58,6 +59,9 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command {
5859
flags.Var(&opts.hosts, flagHost, "Set one or more custom host-to-IP mappings (host:ip)")
5960
flags.SetAnnotation(flagHost, "version", []string{"1.25"})
6061

62+
flags.Var(cliopts.NewListOptsRef(&opts.resources.resGenericResources, ValidateSingleGenericResource), "generic-resource", "User defined resources")
63+
flags.SetAnnotation(flagHostAdd, "version", []string{"1.32"})
64+
6165
flags.SetInterspersed(false)
6266
return cmd
6367
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package service
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/pkg/errors"
8+
9+
"github.com/docker/docker/api/types/swarm"
10+
swarmapi "github.com/docker/swarmkit/api"
11+
"github.com/docker/swarmkit/api/genericresource"
12+
)
13+
14+
// GenericResource is a concept that a user can use to advertise user-defined
15+
// resources on a node and thus better place services based on these resources.
16+
// E.g: NVIDIA GPUs, Intel FPGAs, ...
17+
// See https://github.com/docker/swarmkit/blob/master/design/generic_resources.md
18+
19+
// ValidateSingleGenericResource validates that a single entry in the
20+
// generic resource list is valid.
21+
// i.e 'GPU=UID1' is valid however 'GPU:UID1' or 'UID1' isn't
22+
func ValidateSingleGenericResource(val string) (string, error) {
23+
if strings.Count(val, "=") < 1 {
24+
return "", fmt.Errorf("invalid generic-resource format `%s` expected `name=value`", val)
25+
}
26+
27+
return val, nil
28+
}
29+
30+
// ParseGenericResources parses an array of Generic resourceResources
31+
// Requesting Named Generic Resources for a service is not supported this
32+
// is filtered here.
33+
func ParseGenericResources(value []string) ([]swarm.GenericResource, error) {
34+
if len(value) == 0 {
35+
return nil, nil
36+
}
37+
38+
resources, err := genericresource.Parse(value)
39+
if err != nil {
40+
return nil, errors.Wrapf(err, "invalid generic resource specification")
41+
}
42+
43+
swarmResources := genericResourcesFromGRPC(resources)
44+
for _, res := range swarmResources {
45+
if res.NamedResourceSpec != nil {
46+
return nil, fmt.Errorf("invalid generic-resource request `%s=%s`, Named Generic Resources is not supported for service create or update", res.NamedResourceSpec.Kind, res.NamedResourceSpec.Value)
47+
}
48+
}
49+
50+
return swarmResources, nil
51+
}
52+
53+
// genericResourcesFromGRPC converts a GRPC GenericResource to a GenericResource
54+
func genericResourcesFromGRPC(genericRes []*swarmapi.GenericResource) []swarm.GenericResource {
55+
var generic []swarm.GenericResource
56+
for _, res := range genericRes {
57+
var current swarm.GenericResource
58+
59+
switch r := res.Resource.(type) {
60+
case *swarmapi.GenericResource_DiscreteResourceSpec:
61+
current.DiscreteResourceSpec = &swarm.DiscreteGenericResource{
62+
Kind: r.DiscreteResourceSpec.Kind,
63+
Value: r.DiscreteResourceSpec.Value,
64+
}
65+
case *swarmapi.GenericResource_NamedResourceSpec:
66+
current.NamedResourceSpec = &swarm.NamedGenericResource{
67+
Kind: r.NamedResourceSpec.Kind,
68+
Value: r.NamedResourceSpec.Value,
69+
}
70+
}
71+
72+
generic = append(generic, current)
73+
}
74+
75+
return generic
76+
}
77+
78+
func buildGenericResourceMap(genericRes []swarm.GenericResource) (map[string]swarm.GenericResource, error) {
79+
m := make(map[string]swarm.GenericResource)
80+
81+
for _, res := range genericRes {
82+
if res.DiscreteResourceSpec == nil {
83+
return nil, fmt.Errorf("invalid generic-resource `%+v` for service task", res)
84+
}
85+
86+
_, ok := m[res.DiscreteResourceSpec.Kind]
87+
if ok {
88+
return nil, fmt.Errorf("duplicate generic-resource `%+v` for service task", res.DiscreteResourceSpec.Kind)
89+
}
90+
91+
m[res.DiscreteResourceSpec.Kind] = res
92+
}
93+
94+
return m, nil
95+
}
96+
97+
func buildGenericResourceList(genericRes map[string]swarm.GenericResource) []swarm.GenericResource {
98+
var l []swarm.GenericResource
99+
100+
for _, res := range genericRes {
101+
l = append(l, res)
102+
}
103+
104+
return l
105+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package service
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestValidateSingleGenericResource(t *testing.T) {
10+
incorrect := []string{"foo", "fooo-bar"}
11+
correct := []string{"foo=bar", "bar=1", "foo=barbar"}
12+
13+
for _, v := range incorrect {
14+
_, err := ValidateSingleGenericResource(v)
15+
assert.Error(t, err)
16+
}
17+
18+
for _, v := range correct {
19+
_, err := ValidateSingleGenericResource(v)
20+
assert.NoError(t, err)
21+
}
22+
}

cli/command/service/opts.go

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -222,23 +222,30 @@ func (opts updateOptions) rollbackConfig(flags *pflag.FlagSet) *swarm.UpdateConf
222222
}
223223

224224
type resourceOptions struct {
225-
limitCPU opts.NanoCPUs
226-
limitMemBytes opts.MemBytes
227-
resCPU opts.NanoCPUs
228-
resMemBytes opts.MemBytes
225+
limitCPU opts.NanoCPUs
226+
limitMemBytes opts.MemBytes
227+
resCPU opts.NanoCPUs
228+
resMemBytes opts.MemBytes
229+
resGenericResources []string
229230
}
230231

231-
func (r *resourceOptions) ToResourceRequirements() *swarm.ResourceRequirements {
232+
func (r *resourceOptions) ToResourceRequirements() (*swarm.ResourceRequirements, error) {
233+
generic, err := ParseGenericResources(r.resGenericResources)
234+
if err != nil {
235+
return nil, err
236+
}
237+
232238
return &swarm.ResourceRequirements{
233239
Limits: &swarm.Resources{
234240
NanoCPUs: r.limitCPU.Value(),
235241
MemoryBytes: r.limitMemBytes.Value(),
236242
},
237243
Reservations: &swarm.Resources{
238-
NanoCPUs: r.resCPU.Value(),
239-
MemoryBytes: r.resMemBytes.Value(),
244+
NanoCPUs: r.resCPU.Value(),
245+
MemoryBytes: r.resMemBytes.Value(),
246+
GenericResources: generic,
240247
},
241-
}
248+
}, nil
242249
}
243250

244251
type restartPolicyOptions struct {
@@ -588,6 +595,11 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N
588595
return service, err
589596
}
590597

598+
resources, err := options.resources.ToResourceRequirements()
599+
if err != nil {
600+
return service, err
601+
}
602+
591603
service = swarm.ServiceSpec{
592604
Annotations: swarm.Annotations{
593605
Name: options.name,
@@ -619,7 +631,7 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N
619631
Isolation: container.Isolation(options.isolation),
620632
},
621633
Networks: networks,
622-
Resources: options.resources.ToResourceRequirements(),
634+
Resources: resources,
623635
RestartPolicy: options.restartPolicy.ToRestartPolicy(flags),
624636
Placement: &swarm.Placement{
625637
Constraints: options.constraints.GetAll(),
@@ -818,6 +830,8 @@ const (
818830
flagEnvFile = "env-file"
819831
flagEnvRemove = "env-rm"
820832
flagEnvAdd = "env-add"
833+
flagGenericResourcesRemove = "generic-resource-rm"
834+
flagGenericResourcesAdd = "generic-resource-add"
821835
flagGroup = "group"
822836
flagGroupAdd = "group-add"
823837
flagGroupRemove = "group-rm"

cli/command/service/opts_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,41 @@ func TestHealthCheckOptionsToHealthConfigConflict(t *testing.T) {
8585
_, err := opt.toHealthConfig()
8686
assert.EqualError(t, err, "--no-healthcheck conflicts with --health-* options")
8787
}
88+
89+
func TestResourceOptionsToResourceRequirements(t *testing.T) {
90+
incorrectOptions := []resourceOptions{
91+
{
92+
resGenericResources: []string{"foo=bar", "foo=1"},
93+
},
94+
{
95+
resGenericResources: []string{"foo=bar", "foo=baz"},
96+
},
97+
{
98+
resGenericResources: []string{"foo=bar"},
99+
},
100+
{
101+
resGenericResources: []string{"foo=1", "foo=2"},
102+
},
103+
}
104+
105+
for _, opt := range incorrectOptions {
106+
_, err := opt.ToResourceRequirements()
107+
assert.Error(t, err)
108+
}
109+
110+
correctOptions := []resourceOptions{
111+
{
112+
resGenericResources: []string{"foo=1"},
113+
},
114+
{
115+
resGenericResources: []string{"foo=1", "bar=2"},
116+
},
117+
}
118+
119+
for _, opt := range correctOptions {
120+
r, err := opt.ToResourceRequirements()
121+
assert.NoError(t, err)
122+
assert.Len(t, r.Reservations.GenericResources, len(opt.resGenericResources))
123+
}
124+
125+
}

cli/command/service/update.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,23 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command {
9595
flags.Var(&options.hosts, flagHostAdd, "Add a custom host-to-IP mapping (host:ip)")
9696
flags.SetAnnotation(flagHostAdd, "version", []string{"1.25"})
9797

98+
// Add needs parsing, Remove only needs the key
99+
flags.Var(newListOptsVar(), flagGenericResourcesRemove, "Remove a Generic resource")
100+
flags.SetAnnotation(flagHostAdd, "version", []string{"1.32"})
101+
flags.Var(newListOptsVarWithValidator(ValidateSingleGenericResource), flagGenericResourcesAdd, "Add a Generic resource")
102+
flags.SetAnnotation(flagHostAdd, "version", []string{"1.32"})
103+
98104
return cmd
99105
}
100106

101107
func newListOptsVar() *opts.ListOpts {
102108
return opts.NewListOptsRef(&[]string{}, nil)
103109
}
104110

111+
func newListOptsVarWithValidator(validator opts.ValidatorFctType) *opts.ListOpts {
112+
return opts.NewListOptsRef(&[]string{}, validator)
113+
}
114+
105115
// nolint: gocyclo
106116
func runUpdate(dockerCli command.Cli, flags *pflag.FlagSet, options *serviceOptions, serviceID string) error {
107117
apiClient := dockerCli.Client()
@@ -314,6 +324,14 @@ func updateService(ctx context.Context, apiClient client.NetworkAPIClient, flags
314324
updateInt64Value(flagReserveMemory, &task.Resources.Reservations.MemoryBytes)
315325
}
316326

327+
if err := addGenericResources(flags, task); err != nil {
328+
return err
329+
}
330+
331+
if err := removeGenericResources(flags, task); err != nil {
332+
return err
333+
}
334+
317335
updateDurationOpt(flagStopGracePeriod, &cspec.StopGracePeriod)
318336

319337
if anyChanged(flags, flagRestartCondition, flagRestartDelay, flagRestartMaxAttempts, flagRestartWindow) {
@@ -470,6 +488,72 @@ func anyChanged(flags *pflag.FlagSet, fields ...string) bool {
470488
return false
471489
}
472490

491+
func addGenericResources(flags *pflag.FlagSet, spec *swarm.TaskSpec) error {
492+
if !flags.Changed(flagGenericResourcesAdd) {
493+
return nil
494+
}
495+
496+
if spec.Resources == nil {
497+
spec.Resources = &swarm.ResourceRequirements{}
498+
}
499+
500+
if spec.Resources.Reservations == nil {
501+
spec.Resources.Reservations = &swarm.Resources{}
502+
}
503+
504+
values := flags.Lookup(flagGenericResourcesAdd).Value.(*opts.ListOpts).GetAll()
505+
generic, err := ParseGenericResources(values)
506+
if err != nil {
507+
return err
508+
}
509+
510+
m, err := buildGenericResourceMap(spec.Resources.Reservations.GenericResources)
511+
if err != nil {
512+
return err
513+
}
514+
515+
for _, toAddRes := range generic {
516+
m[toAddRes.DiscreteResourceSpec.Kind] = toAddRes
517+
}
518+
519+
spec.Resources.Reservations.GenericResources = buildGenericResourceList(m)
520+
521+
return nil
522+
}
523+
524+
func removeGenericResources(flags *pflag.FlagSet, spec *swarm.TaskSpec) error {
525+
// Can only be Discrete Resources
526+
if !flags.Changed(flagGenericResourcesRemove) {
527+
return nil
528+
}
529+
530+
if spec.Resources == nil {
531+
spec.Resources = &swarm.ResourceRequirements{}
532+
}
533+
534+
if spec.Resources.Reservations == nil {
535+
spec.Resources.Reservations = &swarm.Resources{}
536+
}
537+
538+
values := flags.Lookup(flagGenericResourcesRemove).Value.(*opts.ListOpts).GetAll()
539+
540+
m, err := buildGenericResourceMap(spec.Resources.Reservations.GenericResources)
541+
if err != nil {
542+
return err
543+
}
544+
545+
for _, toRemoveRes := range values {
546+
if _, ok := m[toRemoveRes]; !ok {
547+
return fmt.Errorf("could not find generic-resource `%s` to remove it", toRemoveRes)
548+
}
549+
550+
delete(m, toRemoveRes)
551+
}
552+
553+
spec.Resources.Reservations.GenericResources = buildGenericResourceList(m)
554+
return nil
555+
}
556+
473557
func updatePlacementConstraints(flags *pflag.FlagSet, placement *swarm.Placement) {
474558
if flags.Changed(flagConstraintAdd) {
475559
values := flags.Lookup(flagConstraintAdd).Value.(*opts.ListOpts).GetAll()

0 commit comments

Comments
 (0)