github.com/kim0/docker@v0.6.2-0.20161130212042-4addda3f07e7/cli/command/service/update.go (about)

     1  package service
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strings"
     7  	"time"
     8  
     9  	"golang.org/x/net/context"
    10  
    11  	"github.com/docker/docker/api/types"
    12  	"github.com/docker/docker/api/types/container"
    13  	mounttypes "github.com/docker/docker/api/types/mount"
    14  	"github.com/docker/docker/api/types/swarm"
    15  	"github.com/docker/docker/cli"
    16  	"github.com/docker/docker/cli/command"
    17  	"github.com/docker/docker/opts"
    18  	runconfigopts "github.com/docker/docker/runconfig/opts"
    19  	"github.com/docker/go-connections/nat"
    20  	shlex "github.com/flynn-archive/go-shlex"
    21  	"github.com/spf13/cobra"
    22  	"github.com/spf13/pflag"
    23  )
    24  
    25  func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
    26  	opts := newServiceOptions()
    27  
    28  	cmd := &cobra.Command{
    29  		Use:   "update [OPTIONS] SERVICE",
    30  		Short: "Update a service",
    31  		Args:  cli.ExactArgs(1),
    32  		RunE: func(cmd *cobra.Command, args []string) error {
    33  			return runUpdate(dockerCli, cmd.Flags(), args[0])
    34  		},
    35  	}
    36  
    37  	flags := cmd.Flags()
    38  	flags.String("image", "", "Service image tag")
    39  	flags.String("args", "", "Service command args")
    40  	flags.Bool("rollback", false, "Rollback to previous specification")
    41  	flags.Bool("force", false, "Force update even if no changes require it")
    42  	addServiceFlags(cmd, opts)
    43  
    44  	flags.Var(newListOptsVar(), flagEnvRemove, "Remove an environment variable")
    45  	flags.Var(newListOptsVar(), flagGroupRemove, "Remove previously added supplementary user groups from the container")
    46  	flags.Var(newListOptsVar(), flagLabelRemove, "Remove a label by its key")
    47  	flags.Var(newListOptsVar(), flagContainerLabelRemove, "Remove a container label by its key")
    48  	flags.Var(newListOptsVar(), flagMountRemove, "Remove a mount by its target path")
    49  	flags.Var(newListOptsVar(), flagPublishRemove, "Remove a published port by its target port")
    50  	flags.Var(newListOptsVar(), flagConstraintRemove, "Remove a constraint")
    51  	flags.Var(&opts.labels, flagLabelAdd, "Add or update service labels")
    52  	flags.Var(&opts.containerLabels, flagContainerLabelAdd, "Add or update container labels")
    53  	flags.Var(&opts.env, flagEnvAdd, "Add or update environment variables")
    54  	flags.Var(&opts.mounts, flagMountAdd, "Add or update a mount on a service")
    55  	flags.StringSliceVar(&opts.constraints, flagConstraintAdd, []string{}, "Add or update placement constraints")
    56  	flags.Var(&opts.endpoint.ports, flagPublishAdd, "Add or update a published port")
    57  	flags.StringSliceVar(&opts.groups, flagGroupAdd, []string{}, "Add additional supplementary user groups to the container")
    58  	return cmd
    59  }
    60  
    61  func newListOptsVar() *opts.ListOpts {
    62  	return opts.NewListOptsRef(&[]string{}, nil)
    63  }
    64  
    65  func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID string) error {
    66  	apiClient := dockerCli.Client()
    67  	ctx := context.Background()
    68  	updateOpts := types.ServiceUpdateOptions{}
    69  
    70  	service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID)
    71  	if err != nil {
    72  		return err
    73  	}
    74  
    75  	rollback, err := flags.GetBool("rollback")
    76  	if err != nil {
    77  		return err
    78  	}
    79  
    80  	spec := &service.Spec
    81  	if rollback {
    82  		spec = service.PreviousSpec
    83  		if spec == nil {
    84  			return fmt.Errorf("service does not have a previous specification to roll back to")
    85  		}
    86  	}
    87  
    88  	err = updateService(flags, spec)
    89  	if err != nil {
    90  		return err
    91  	}
    92  
    93  	// only send auth if flag was set
    94  	sendAuth, err := flags.GetBool(flagRegistryAuth)
    95  	if err != nil {
    96  		return err
    97  	}
    98  	if sendAuth {
    99  		// Retrieve encoded auth token from the image reference
   100  		// This would be the old image if it didn't change in this update
   101  		image := spec.TaskTemplate.ContainerSpec.Image
   102  		encodedAuth, err := command.RetrieveAuthTokenFromImage(ctx, dockerCli, image)
   103  		if err != nil {
   104  			return err
   105  		}
   106  		updateOpts.EncodedRegistryAuth = encodedAuth
   107  	} else if rollback {
   108  		updateOpts.RegistryAuthFrom = types.RegistryAuthFromPreviousSpec
   109  	} else {
   110  		updateOpts.RegistryAuthFrom = types.RegistryAuthFromSpec
   111  	}
   112  
   113  	err = apiClient.ServiceUpdate(ctx, service.ID, service.Version, *spec, updateOpts)
   114  	if err != nil {
   115  		return err
   116  	}
   117  
   118  	fmt.Fprintf(dockerCli.Out(), "%s\n", serviceID)
   119  	return nil
   120  }
   121  
   122  func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error {
   123  	updateString := func(flag string, field *string) {
   124  		if flags.Changed(flag) {
   125  			*field, _ = flags.GetString(flag)
   126  		}
   127  	}
   128  
   129  	updateInt64Value := func(flag string, field *int64) {
   130  		if flags.Changed(flag) {
   131  			*field = flags.Lookup(flag).Value.(int64Value).Value()
   132  		}
   133  	}
   134  
   135  	updateFloat32 := func(flag string, field *float32) {
   136  		if flags.Changed(flag) {
   137  			*field, _ = flags.GetFloat32(flag)
   138  		}
   139  	}
   140  
   141  	updateDuration := func(flag string, field *time.Duration) {
   142  		if flags.Changed(flag) {
   143  			*field, _ = flags.GetDuration(flag)
   144  		}
   145  	}
   146  
   147  	updateDurationOpt := func(flag string, field **time.Duration) {
   148  		if flags.Changed(flag) {
   149  			val := *flags.Lookup(flag).Value.(*DurationOpt).Value()
   150  			*field = &val
   151  		}
   152  	}
   153  
   154  	updateUint64 := func(flag string, field *uint64) {
   155  		if flags.Changed(flag) {
   156  			*field, _ = flags.GetUint64(flag)
   157  		}
   158  	}
   159  
   160  	updateUint64Opt := func(flag string, field **uint64) {
   161  		if flags.Changed(flag) {
   162  			val := *flags.Lookup(flag).Value.(*Uint64Opt).Value()
   163  			*field = &val
   164  		}
   165  	}
   166  
   167  	cspec := &spec.TaskTemplate.ContainerSpec
   168  	task := &spec.TaskTemplate
   169  
   170  	taskResources := func() *swarm.ResourceRequirements {
   171  		if task.Resources == nil {
   172  			task.Resources = &swarm.ResourceRequirements{}
   173  		}
   174  		return task.Resources
   175  	}
   176  
   177  	updateLabels(flags, &spec.Labels)
   178  	updateContainerLabels(flags, &cspec.Labels)
   179  	updateString("image", &cspec.Image)
   180  	updateStringToSlice(flags, "args", &cspec.Args)
   181  	updateEnvironment(flags, &cspec.Env)
   182  	updateString(flagWorkdir, &cspec.Dir)
   183  	updateString(flagUser, &cspec.User)
   184  	updateMounts(flags, &cspec.Mounts)
   185  
   186  	if flags.Changed(flagLimitCPU) || flags.Changed(flagLimitMemory) {
   187  		taskResources().Limits = &swarm.Resources{}
   188  		updateInt64Value(flagLimitCPU, &task.Resources.Limits.NanoCPUs)
   189  		updateInt64Value(flagLimitMemory, &task.Resources.Limits.MemoryBytes)
   190  	}
   191  	if flags.Changed(flagReserveCPU) || flags.Changed(flagReserveMemory) {
   192  		taskResources().Reservations = &swarm.Resources{}
   193  		updateInt64Value(flagReserveCPU, &task.Resources.Reservations.NanoCPUs)
   194  		updateInt64Value(flagReserveMemory, &task.Resources.Reservations.MemoryBytes)
   195  	}
   196  
   197  	updateDurationOpt(flagStopGracePeriod, &cspec.StopGracePeriod)
   198  
   199  	if anyChanged(flags, flagRestartCondition, flagRestartDelay, flagRestartMaxAttempts, flagRestartWindow) {
   200  		if task.RestartPolicy == nil {
   201  			task.RestartPolicy = &swarm.RestartPolicy{}
   202  		}
   203  
   204  		if flags.Changed(flagRestartCondition) {
   205  			value, _ := flags.GetString(flagRestartCondition)
   206  			task.RestartPolicy.Condition = swarm.RestartPolicyCondition(value)
   207  		}
   208  		updateDurationOpt(flagRestartDelay, &task.RestartPolicy.Delay)
   209  		updateUint64Opt(flagRestartMaxAttempts, &task.RestartPolicy.MaxAttempts)
   210  		updateDurationOpt(flagRestartWindow, &task.RestartPolicy.Window)
   211  	}
   212  
   213  	if anyChanged(flags, flagConstraintAdd, flagConstraintRemove) {
   214  		if task.Placement == nil {
   215  			task.Placement = &swarm.Placement{}
   216  		}
   217  		updatePlacement(flags, task.Placement)
   218  	}
   219  
   220  	if err := updateReplicas(flags, &spec.Mode); err != nil {
   221  		return err
   222  	}
   223  
   224  	if anyChanged(flags, flagUpdateParallelism, flagUpdateDelay, flagUpdateMonitor, flagUpdateFailureAction, flagUpdateMaxFailureRatio) {
   225  		if spec.UpdateConfig == nil {
   226  			spec.UpdateConfig = &swarm.UpdateConfig{}
   227  		}
   228  		updateUint64(flagUpdateParallelism, &spec.UpdateConfig.Parallelism)
   229  		updateDuration(flagUpdateDelay, &spec.UpdateConfig.Delay)
   230  		updateDuration(flagUpdateMonitor, &spec.UpdateConfig.Monitor)
   231  		updateString(flagUpdateFailureAction, &spec.UpdateConfig.FailureAction)
   232  		updateFloat32(flagUpdateMaxFailureRatio, &spec.UpdateConfig.MaxFailureRatio)
   233  	}
   234  
   235  	if flags.Changed(flagEndpointMode) {
   236  		value, _ := flags.GetString(flagEndpointMode)
   237  		if spec.EndpointSpec == nil {
   238  			spec.EndpointSpec = &swarm.EndpointSpec{}
   239  		}
   240  		spec.EndpointSpec.Mode = swarm.ResolutionMode(value)
   241  	}
   242  
   243  	if anyChanged(flags, flagGroupAdd, flagGroupRemove) {
   244  		if err := updateGroups(flags, &cspec.Groups); err != nil {
   245  			return err
   246  		}
   247  	}
   248  
   249  	if anyChanged(flags, flagPublishAdd, flagPublishRemove) {
   250  		if spec.EndpointSpec == nil {
   251  			spec.EndpointSpec = &swarm.EndpointSpec{}
   252  		}
   253  		if err := updatePorts(flags, &spec.EndpointSpec.Ports); err != nil {
   254  			return err
   255  		}
   256  	}
   257  
   258  	if err := updateLogDriver(flags, &spec.TaskTemplate); err != nil {
   259  		return err
   260  	}
   261  
   262  	force, err := flags.GetBool("force")
   263  	if err != nil {
   264  		return err
   265  	}
   266  
   267  	if force {
   268  		spec.TaskTemplate.ForceUpdate++
   269  	}
   270  
   271  	if err := updateHealthcheck(flags, cspec); err != nil {
   272  		return err
   273  	}
   274  
   275  	return nil
   276  }
   277  
   278  func updateStringToSlice(flags *pflag.FlagSet, flag string, field *[]string) error {
   279  	if !flags.Changed(flag) {
   280  		return nil
   281  	}
   282  
   283  	value, _ := flags.GetString(flag)
   284  	valueSlice, err := shlex.Split(value)
   285  	*field = valueSlice
   286  	return err
   287  }
   288  
   289  func anyChanged(flags *pflag.FlagSet, fields ...string) bool {
   290  	for _, flag := range fields {
   291  		if flags.Changed(flag) {
   292  			return true
   293  		}
   294  	}
   295  	return false
   296  }
   297  
   298  func updatePlacement(flags *pflag.FlagSet, placement *swarm.Placement) {
   299  	field, _ := flags.GetStringSlice(flagConstraintAdd)
   300  	placement.Constraints = append(placement.Constraints, field...)
   301  
   302  	toRemove := buildToRemoveSet(flags, flagConstraintRemove)
   303  	placement.Constraints = removeItems(placement.Constraints, toRemove, itemKey)
   304  }
   305  
   306  func updateContainerLabels(flags *pflag.FlagSet, field *map[string]string) {
   307  	if flags.Changed(flagContainerLabelAdd) {
   308  		if *field == nil {
   309  			*field = map[string]string{}
   310  		}
   311  
   312  		values := flags.Lookup(flagContainerLabelAdd).Value.(*opts.ListOpts).GetAll()
   313  		for key, value := range runconfigopts.ConvertKVStringsToMap(values) {
   314  			(*field)[key] = value
   315  		}
   316  	}
   317  
   318  	if *field != nil && flags.Changed(flagContainerLabelRemove) {
   319  		toRemove := flags.Lookup(flagContainerLabelRemove).Value.(*opts.ListOpts).GetAll()
   320  		for _, label := range toRemove {
   321  			delete(*field, label)
   322  		}
   323  	}
   324  }
   325  
   326  func updateLabels(flags *pflag.FlagSet, field *map[string]string) {
   327  	if flags.Changed(flagLabelAdd) {
   328  		if *field == nil {
   329  			*field = map[string]string{}
   330  		}
   331  
   332  		values := flags.Lookup(flagLabelAdd).Value.(*opts.ListOpts).GetAll()
   333  		for key, value := range runconfigopts.ConvertKVStringsToMap(values) {
   334  			(*field)[key] = value
   335  		}
   336  	}
   337  
   338  	if *field != nil && flags.Changed(flagLabelRemove) {
   339  		toRemove := flags.Lookup(flagLabelRemove).Value.(*opts.ListOpts).GetAll()
   340  		for _, label := range toRemove {
   341  			delete(*field, label)
   342  		}
   343  	}
   344  }
   345  
   346  func updateEnvironment(flags *pflag.FlagSet, field *[]string) {
   347  	envSet := map[string]string{}
   348  	for _, v := range *field {
   349  		envSet[envKey(v)] = v
   350  	}
   351  	if flags.Changed(flagEnvAdd) {
   352  		value := flags.Lookup(flagEnvAdd).Value.(*opts.ListOpts)
   353  		for _, v := range value.GetAll() {
   354  			envSet[envKey(v)] = v
   355  		}
   356  	}
   357  
   358  	*field = []string{}
   359  	for _, v := range envSet {
   360  		*field = append(*field, v)
   361  	}
   362  
   363  	toRemove := buildToRemoveSet(flags, flagEnvRemove)
   364  	*field = removeItems(*field, toRemove, envKey)
   365  }
   366  
   367  func envKey(value string) string {
   368  	kv := strings.SplitN(value, "=", 2)
   369  	return kv[0]
   370  }
   371  
   372  func itemKey(value string) string {
   373  	return value
   374  }
   375  
   376  func buildToRemoveSet(flags *pflag.FlagSet, flag string) map[string]struct{} {
   377  	var empty struct{}
   378  	toRemove := make(map[string]struct{})
   379  
   380  	if !flags.Changed(flag) {
   381  		return toRemove
   382  	}
   383  
   384  	toRemoveSlice := flags.Lookup(flag).Value.(*opts.ListOpts).GetAll()
   385  	for _, key := range toRemoveSlice {
   386  		toRemove[key] = empty
   387  	}
   388  	return toRemove
   389  }
   390  
   391  func removeItems(
   392  	seq []string,
   393  	toRemove map[string]struct{},
   394  	keyFunc func(string) string,
   395  ) []string {
   396  	newSeq := []string{}
   397  	for _, item := range seq {
   398  		if _, exists := toRemove[keyFunc(item)]; !exists {
   399  			newSeq = append(newSeq, item)
   400  		}
   401  	}
   402  	return newSeq
   403  }
   404  
   405  func updateMounts(flags *pflag.FlagSet, mounts *[]mounttypes.Mount) {
   406  	if flags.Changed(flagMountAdd) {
   407  		values := flags.Lookup(flagMountAdd).Value.(*MountOpt).Value()
   408  		*mounts = append(*mounts, values...)
   409  	}
   410  	toRemove := buildToRemoveSet(flags, flagMountRemove)
   411  
   412  	newMounts := []mounttypes.Mount{}
   413  	for _, mount := range *mounts {
   414  		if _, exists := toRemove[mount.Target]; !exists {
   415  			newMounts = append(newMounts, mount)
   416  		}
   417  	}
   418  	*mounts = newMounts
   419  }
   420  
   421  func updateGroups(flags *pflag.FlagSet, groups *[]string) error {
   422  	if flags.Changed(flagGroupAdd) {
   423  		values, err := flags.GetStringSlice(flagGroupAdd)
   424  		if err != nil {
   425  			return err
   426  		}
   427  		*groups = append(*groups, values...)
   428  	}
   429  	toRemove := buildToRemoveSet(flags, flagGroupRemove)
   430  
   431  	newGroups := []string{}
   432  	for _, group := range *groups {
   433  		if _, exists := toRemove[group]; !exists {
   434  			newGroups = append(newGroups, group)
   435  		}
   436  	}
   437  	// Sort so that result is predictable.
   438  	sort.Strings(newGroups)
   439  
   440  	*groups = newGroups
   441  	return nil
   442  }
   443  
   444  type byPortConfig []swarm.PortConfig
   445  
   446  func (r byPortConfig) Len() int      { return len(r) }
   447  func (r byPortConfig) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
   448  func (r byPortConfig) Less(i, j int) bool {
   449  	// We convert PortConfig into `port/protocol`, e.g., `80/tcp`
   450  	// In updatePorts we already filter out with map so there is duplicate entries
   451  	return portConfigToString(&r[i]) < portConfigToString(&r[j])
   452  }
   453  
   454  func portConfigToString(portConfig *swarm.PortConfig) string {
   455  	protocol := portConfig.Protocol
   456  	if protocol == "" {
   457  		protocol = "tcp"
   458  	}
   459  	return fmt.Sprintf("%v/%s", portConfig.PublishedPort, protocol)
   460  }
   461  
   462  func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error {
   463  	// The key of the map is `port/protocol`, e.g., `80/tcp`
   464  	portSet := map[string]swarm.PortConfig{}
   465  	// Check to see if there are any conflict in flags.
   466  	if flags.Changed(flagPublishAdd) {
   467  		values := flags.Lookup(flagPublishAdd).Value.(*opts.ListOpts).GetAll()
   468  		ports, portBindings, _ := nat.ParsePortSpecs(values)
   469  
   470  		for port := range ports {
   471  			newConfigs := convertPortToPortConfig(port, portBindings)
   472  			for _, entry := range newConfigs {
   473  				if v, ok := portSet[portConfigToString(&entry)]; ok && v != entry {
   474  					return fmt.Errorf("conflicting port mapping between %v:%v/%s and %v:%v/%s", entry.PublishedPort, entry.TargetPort, entry.Protocol, v.PublishedPort, v.TargetPort, v.Protocol)
   475  				}
   476  				portSet[portConfigToString(&entry)] = entry
   477  			}
   478  		}
   479  	}
   480  
   481  	// Override previous PortConfig in service if there is any duplicate
   482  	for _, entry := range *portConfig {
   483  		if _, ok := portSet[portConfigToString(&entry)]; !ok {
   484  			portSet[portConfigToString(&entry)] = entry
   485  		}
   486  	}
   487  
   488  	toRemove := flags.Lookup(flagPublishRemove).Value.(*opts.ListOpts).GetAll()
   489  	newPorts := []swarm.PortConfig{}
   490  portLoop:
   491  	for _, port := range portSet {
   492  		for _, rawTargetPort := range toRemove {
   493  			targetPort := nat.Port(rawTargetPort)
   494  			if equalPort(targetPort, port) {
   495  				continue portLoop
   496  			}
   497  		}
   498  		newPorts = append(newPorts, port)
   499  	}
   500  	// Sort the PortConfig to avoid unnecessary updates
   501  	sort.Sort(byPortConfig(newPorts))
   502  	*portConfig = newPorts
   503  	return nil
   504  }
   505  
   506  func equalPort(targetPort nat.Port, port swarm.PortConfig) bool {
   507  	return (string(port.Protocol) == targetPort.Proto() &&
   508  		port.TargetPort == uint32(targetPort.Int()))
   509  }
   510  
   511  func updateReplicas(flags *pflag.FlagSet, serviceMode *swarm.ServiceMode) error {
   512  	if !flags.Changed(flagReplicas) {
   513  		return nil
   514  	}
   515  
   516  	if serviceMode == nil || serviceMode.Replicated == nil {
   517  		return fmt.Errorf("replicas can only be used with replicated mode")
   518  	}
   519  	serviceMode.Replicated.Replicas = flags.Lookup(flagReplicas).Value.(*Uint64Opt).Value()
   520  	return nil
   521  }
   522  
   523  // updateLogDriver updates the log driver only if the log driver flag is set.
   524  // All options will be replaced with those provided on the command line.
   525  func updateLogDriver(flags *pflag.FlagSet, taskTemplate *swarm.TaskSpec) error {
   526  	if !flags.Changed(flagLogDriver) {
   527  		return nil
   528  	}
   529  
   530  	name, err := flags.GetString(flagLogDriver)
   531  	if err != nil {
   532  		return err
   533  	}
   534  
   535  	if name == "" {
   536  		return nil
   537  	}
   538  
   539  	taskTemplate.LogDriver = &swarm.Driver{
   540  		Name:    name,
   541  		Options: runconfigopts.ConvertKVStringsToMap(flags.Lookup(flagLogOpt).Value.(*opts.ListOpts).GetAll()),
   542  	}
   543  
   544  	return nil
   545  }
   546  
   547  func updateHealthcheck(flags *pflag.FlagSet, containerSpec *swarm.ContainerSpec) error {
   548  	if !anyChanged(flags, flagNoHealthcheck, flagHealthCmd, flagHealthInterval, flagHealthRetries, flagHealthTimeout) {
   549  		return nil
   550  	}
   551  	if containerSpec.Healthcheck == nil {
   552  		containerSpec.Healthcheck = &container.HealthConfig{}
   553  	}
   554  	noHealthcheck, err := flags.GetBool(flagNoHealthcheck)
   555  	if err != nil {
   556  		return err
   557  	}
   558  	if noHealthcheck {
   559  		if !anyChanged(flags, flagHealthCmd, flagHealthInterval, flagHealthRetries, flagHealthTimeout) {
   560  			containerSpec.Healthcheck = &container.HealthConfig{
   561  				Test: []string{"NONE"},
   562  			}
   563  			return nil
   564  		}
   565  		return fmt.Errorf("--%s conflicts with --health-* options", flagNoHealthcheck)
   566  	}
   567  	if len(containerSpec.Healthcheck.Test) > 0 && containerSpec.Healthcheck.Test[0] == "NONE" {
   568  		containerSpec.Healthcheck.Test = nil
   569  	}
   570  	if flags.Changed(flagHealthInterval) {
   571  		val := *flags.Lookup(flagHealthInterval).Value.(*PositiveDurationOpt).Value()
   572  		containerSpec.Healthcheck.Interval = val
   573  	}
   574  	if flags.Changed(flagHealthTimeout) {
   575  		val := *flags.Lookup(flagHealthTimeout).Value.(*PositiveDurationOpt).Value()
   576  		containerSpec.Healthcheck.Timeout = val
   577  	}
   578  	if flags.Changed(flagHealthRetries) {
   579  		containerSpec.Healthcheck.Retries, _ = flags.GetInt(flagHealthRetries)
   580  	}
   581  	if flags.Changed(flagHealthCmd) {
   582  		cmd, _ := flags.GetString(flagHealthCmd)
   583  		if cmd != "" {
   584  			containerSpec.Healthcheck.Test = []string{"CMD-SHELL", cmd}
   585  		} else {
   586  			containerSpec.Healthcheck.Test = nil
   587  		}
   588  	}
   589  	return nil
   590  }