diff --git a/cli/command/service/create.go b/cli/command/service/create.go index 299cdc383f..6e19f8b748 100644 --- a/cli/command/service/create.go +++ b/cli/command/service/create.go @@ -77,6 +77,8 @@ func runCreate(dockerCli command.Cli, flags *pflag.FlagSet, opts *serviceOptions return err } + fmt.Printf("%v\n", service.TaskTemplate.Resources) + specifiedSecrets := opts.secrets.Value() if len(specifiedSecrets) > 0 { // parse and validate secrets diff --git a/cli/command/service/generic_resource_opts.go b/cli/command/service/generic_resource_opts.go index 5effe83968..66385888e1 100644 --- a/cli/command/service/generic_resource_opts.go +++ b/cli/command/service/generic_resource_opts.go @@ -74,3 +74,32 @@ func genericResourcesFromGRPC(genericRes []*swarmapi.GenericResource) []swarm.Ge return generic } + +func buildGenericResourceMap(genericRes []swarm.GenericResource) (map[string]swarm.GenericResource, error) { + m := make(map[string]swarm.GenericResource) + + for _, res := range genericRes { + if res.DiscreteResourceSpec == nil { + return nil, fmt.Errorf("invalid generic-resource `%+v` for service task", res) + } + + _, ok := m[res.DiscreteResourceSpec.Kind] + if ok { + return nil, fmt.Errorf("duplicate generic-resource `%+v` for service task", res.DiscreteResourceSpec.Kind) + } + + m[res.DiscreteResourceSpec.Kind] = res + } + + return m, nil +} + +func buildGenericResourceList(genericRes map[string]swarm.GenericResource) []swarm.GenericResource { + var l []swarm.GenericResource + + for _, res := range genericRes { + l = append(l, res) + } + + return l +} diff --git a/cli/command/service/opts.go b/cli/command/service/opts.go index 209167dfda..b57c5c8c4e 100644 --- a/cli/command/service/opts.go +++ b/cli/command/service/opts.go @@ -830,6 +830,8 @@ const ( flagEnvFile = "env-file" flagEnvRemove = "env-rm" flagEnvAdd = "env-add" + flagGenericResourcesRemove = "generic-resource-rm" + flagGenericResourcesAdd = "generic-resource-add" flagGroup = "group" flagGroupAdd = "group-add" flagGroupRemove = "group-rm" diff --git a/cli/command/service/update.go b/cli/command/service/update.go index a7c82ec0c1..d657871ff2 100644 --- a/cli/command/service/update.go +++ b/cli/command/service/update.go @@ -95,6 +95,12 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command { flags.Var(&options.hosts, flagHostAdd, "Add a custom host-to-IP mapping (host:ip)") flags.SetAnnotation(flagHostAdd, "version", []string{"1.25"}) + // Add needs parsing, Remove only needs the key + flags.Var(newListOptsVar(), flagGenericResourcesRemove, "Remove a Generic resource") + flags.SetAnnotation(flagHostAdd, "version", []string{"1.32"}) + flags.Var(newListOptsVarWithValidator(ValidateSingleGenericResource), flagGenericResourcesAdd, "Add a Generic resource") + flags.SetAnnotation(flagHostAdd, "version", []string{"1.32"}) + return cmd } @@ -102,6 +108,10 @@ func newListOptsVar() *opts.ListOpts { return opts.NewListOptsRef(&[]string{}, nil) } +func newListOptsVarWithValidator(validator opts.ValidatorFctType) *opts.ListOpts { + return opts.NewListOptsRef(&[]string{}, validator) +} + // nolint: gocyclo func runUpdate(dockerCli command.Cli, flags *pflag.FlagSet, options *serviceOptions, serviceID string) error { apiClient := dockerCli.Client() @@ -314,6 +324,14 @@ func updateService(ctx context.Context, apiClient client.NetworkAPIClient, flags updateInt64Value(flagReserveMemory, &task.Resources.Reservations.MemoryBytes) } + if err := addGenericResources(flags, task); err != nil { + return err + } + + if err := removeGenericResources(flags, task); err != nil { + return err + } + updateDurationOpt(flagStopGracePeriod, &cspec.StopGracePeriod) if anyChanged(flags, flagRestartCondition, flagRestartDelay, flagRestartMaxAttempts, flagRestartWindow) { @@ -470,6 +488,72 @@ func anyChanged(flags *pflag.FlagSet, fields ...string) bool { return false } +func addGenericResources(flags *pflag.FlagSet, spec *swarm.TaskSpec) error { + if !flags.Changed(flagGenericResourcesAdd) { + return nil + } + + if spec.Resources == nil { + spec.Resources = &swarm.ResourceRequirements{} + } + + if spec.Resources.Reservations == nil { + spec.Resources.Reservations = &swarm.Resources{} + } + + values := flags.Lookup(flagGenericResourcesAdd).Value.(*opts.ListOpts).GetAll() + generic, err := ParseGenericResources(values) + if err != nil { + return err + } + + m, err := buildGenericResourceMap(spec.Resources.Reservations.GenericResources) + if err != nil { + return err + } + + for _, toAddRes := range generic { + m[toAddRes.DiscreteResourceSpec.Kind] = toAddRes + } + + spec.Resources.Reservations.GenericResources = buildGenericResourceList(m) + + return nil +} + +func removeGenericResources(flags *pflag.FlagSet, spec *swarm.TaskSpec) error { + // Can only be Discrete Resources + if !flags.Changed(flagGenericResourcesRemove) { + return nil + } + + if spec.Resources == nil { + spec.Resources = &swarm.ResourceRequirements{} + } + + if spec.Resources.Reservations == nil { + spec.Resources.Reservations = &swarm.Resources{} + } + + values := flags.Lookup(flagGenericResourcesRemove).Value.(*opts.ListOpts).GetAll() + + m, err := buildGenericResourceMap(spec.Resources.Reservations.GenericResources) + if err != nil { + return err + } + + for _, toRemoveRes := range values { + if _, ok := m[toRemoveRes]; !ok { + return fmt.Errorf("could not find generic-resource `%s` to remove it", toRemoveRes) + } + + delete(m, toRemoveRes) + } + + spec.Resources.Reservations.GenericResources = buildGenericResourceList(m) + return nil +} + func updatePlacementConstraints(flags *pflag.FlagSet, placement *swarm.Placement) { if flags.Changed(flagConstraintAdd) { values := flags.Lookup(flagConstraintAdd).Value.(*opts.ListOpts).GetAll() diff --git a/cli/command/service/update_test.go b/cli/command/service/update_test.go index 92b4d6bc59..ba55269cb0 100644 --- a/cli/command/service/update_test.go +++ b/cli/command/service/update_test.go @@ -547,3 +547,42 @@ func TestUpdateIsolationInvalid(t *testing.T) { require.NoError(t, err) assert.Equal(t, container.Isolation("test"), spec.TaskTemplate.ContainerSpec.Isolation) } + +func TestAddGenericResources(t *testing.T) { + task := &swarm.TaskSpec{} + flags := newUpdateCommand(nil).Flags() + + assert.Nil(t, addGenericResources(flags, task)) + + flags.Set(flagGenericResourcesAdd, "foo=1") + assert.NoError(t, addGenericResources(flags, task)) + assert.Len(t, task.Resources.Reservations.GenericResources, 1) + + // Checks that foo isn't added a 2nd time + flags = newUpdateCommand(nil).Flags() + flags.Set(flagGenericResourcesAdd, "bar=1") + assert.NoError(t, addGenericResources(flags, task)) + assert.Len(t, task.Resources.Reservations.GenericResources, 2) +} + +func TestRemoveGenericResources(t *testing.T) { + task := &swarm.TaskSpec{} + flags := newUpdateCommand(nil).Flags() + + assert.Nil(t, removeGenericResources(flags, task)) + + flags.Set(flagGenericResourcesRemove, "foo") + assert.Error(t, removeGenericResources(flags, task)) + + flags = newUpdateCommand(nil).Flags() + flags.Set(flagGenericResourcesAdd, "foo=1") + addGenericResources(flags, task) + flags = newUpdateCommand(nil).Flags() + flags.Set(flagGenericResourcesAdd, "bar=1") + addGenericResources(flags, task) + + flags = newUpdateCommand(nil).Flags() + flags.Set(flagGenericResourcesRemove, "foo") + assert.NoError(t, removeGenericResources(flags, task)) + assert.Len(t, task.Resources.Reservations.GenericResources, 1) +} diff --git a/docs/reference/commandline/service_update.md b/docs/reference/commandline/service_update.md index f57a26e802..ef9e0816d2 100644 --- a/docs/reference/commandline/service_update.md +++ b/docs/reference/commandline/service_update.md @@ -41,6 +41,8 @@ Options: --env-add list Add or update an environment variable --env-rm list Remove an environment variable --force Force update even if no changes require it + --generic-resource-add list Add an additional generic resource to the service's resources requirements + --generic-resource-rm list Remove a previously added generic resource to the service's resources requirements --group-add list Add an additional supplementary user group to the container --group-rm list Remove a previously added supplementary user group from the container --health-cmd string Command to run to check health