github.com/containerd/nerdctl@v1.7.7/cmd/nerdctl/container_update.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package main
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"runtime"
    24  
    25  	"github.com/containerd/containerd"
    26  	"github.com/containerd/containerd/containers"
    27  	"github.com/containerd/containerd/pkg/cri/util"
    28  	"github.com/containerd/errdefs"
    29  	"github.com/containerd/log"
    30  	"github.com/containerd/nerdctl/pkg/api/types"
    31  	"github.com/containerd/nerdctl/pkg/clientutil"
    32  	nerdctlContainer "github.com/containerd/nerdctl/pkg/cmd/container"
    33  	"github.com/containerd/nerdctl/pkg/formatter"
    34  	"github.com/containerd/nerdctl/pkg/idutil/containerwalker"
    35  	"github.com/containerd/nerdctl/pkg/infoutil"
    36  	"github.com/containerd/typeurl/v2"
    37  	"github.com/docker/go-units"
    38  	runtimespec "github.com/opencontainers/runtime-spec/specs-go"
    39  	"github.com/spf13/cobra"
    40  )
    41  
    42  type updateResourceOptions struct {
    43  	CPUPeriod          uint64
    44  	CPUQuota           int64
    45  	CPUShares          uint64
    46  	MemoryLimitInBytes int64
    47  	MemoryReservation  int64
    48  	MemorySwapInBytes  int64
    49  	CpusetCpus         string
    50  	CpusetMems         string
    51  	PidsLimit          int64
    52  	BlkioWeight        uint16
    53  }
    54  
    55  func newUpdateCommand() *cobra.Command {
    56  	var updateCommand = &cobra.Command{
    57  		Use:               "update [flags] CONTAINER [CONTAINER, ...]",
    58  		Args:              cobra.MinimumNArgs(1),
    59  		Short:             "Update one or more running containers",
    60  		RunE:              updateAction,
    61  		ValidArgsFunction: updateShellComplete,
    62  		SilenceUsage:      true,
    63  		SilenceErrors:     true,
    64  	}
    65  	updateCommand.Flags().SetInterspersed(false)
    66  	setUpdateFlags(updateCommand)
    67  	return updateCommand
    68  }
    69  
    70  func setUpdateFlags(cmd *cobra.Command) {
    71  	cmd.Flags().Float64("cpus", 0.0, "Number of CPUs")
    72  	cmd.Flags().Uint64("cpu-period", 0, "Limit CPU CFS (Completely Fair Scheduler) period")
    73  	cmd.Flags().Int64("cpu-quota", -1, "Limit CPU CFS (Completely Fair Scheduler) quota")
    74  	cmd.Flags().Uint64("cpu-shares", 0, "CPU shares (relative weight)")
    75  	cmd.Flags().StringP("memory", "m", "", "Memory limit")
    76  	cmd.Flags().String("memory-reservation", "", "Memory soft limit")
    77  	cmd.Flags().String("memory-swap", "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap")
    78  	cmd.Flags().String("kernel-memory", "", "Kernel memory limit (deprecated)")
    79  	cmd.Flags().String("cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)")
    80  	cmd.Flags().String("cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)")
    81  	cmd.Flags().Int64("pids-limit", -1, "Tune container pids limit (set -1 for unlimited)")
    82  	cmd.Flags().Uint16("blkio-weight", 0, "Block IO (relative weight), between 10 and 1000, or 0 to disable (default 0)")
    83  	cmd.Flags().String("restart", "no", `Restart policy to apply when a container exits (implemented values: "no"|"always|on-failure:n|unless-stopped")`)
    84  	cmd.RegisterFlagCompletionFunc("restart", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
    85  		return []string{"no", "always", "on-failure", "unless-stopped"}, cobra.ShellCompDirectiveNoFileComp
    86  	})
    87  }
    88  
    89  func updateAction(cmd *cobra.Command, args []string) error {
    90  	globalOptions, err := processRootCmdFlags(cmd)
    91  	if err != nil {
    92  		return err
    93  	}
    94  	client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address)
    95  	if err != nil {
    96  		return err
    97  	}
    98  	defer cancel()
    99  	options, err := getUpdateOption(cmd, globalOptions)
   100  	if err != nil {
   101  		return err
   102  	}
   103  	walker := &containerwalker.ContainerWalker{
   104  		Client: client,
   105  		OnFound: func(ctx context.Context, found containerwalker.Found) error {
   106  			if found.MatchCount > 1 {
   107  				return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req)
   108  			}
   109  			err = updateContainer(ctx, client, found.Container.ID(), options, cmd)
   110  			return err
   111  		},
   112  	}
   113  
   114  	return walker.WalkAll(ctx, args, true)
   115  }
   116  
   117  func getUpdateOption(cmd *cobra.Command, globalOptions types.GlobalCommandOptions) (updateResourceOptions, error) {
   118  	var options updateResourceOptions
   119  	cpus, err := cmd.Flags().GetFloat64("cpus")
   120  	if err != nil {
   121  		return options, err
   122  	}
   123  	cpuPeriod, err := cmd.Flags().GetUint64("cpu-period")
   124  	if err != nil {
   125  		return options, err
   126  	}
   127  	cpuQuota, err := cmd.Flags().GetInt64("cpu-quota")
   128  	if err != nil {
   129  		return options, err
   130  	}
   131  	if cpuQuota != -1 || cpuPeriod != 0 {
   132  		if cpus > 0.0 {
   133  			return options, errors.New("cpus and quota/period should be used separately")
   134  		}
   135  	}
   136  	if cpus > 0.0 {
   137  		cpuPeriod = uint64(100000)
   138  		cpuQuota = int64(cpus * 100000.0)
   139  	}
   140  	shares, err := cmd.Flags().GetUint64("cpu-shares")
   141  	if err != nil {
   142  		return options, err
   143  	}
   144  	memStr, err := cmd.Flags().GetString("memory")
   145  	if err != nil {
   146  		return options, err
   147  	}
   148  	memSwap, err := cmd.Flags().GetString("memory-swap")
   149  	if err != nil {
   150  		return options, err
   151  	}
   152  	var mem64 int64
   153  	if memStr != "" {
   154  		mem64, err = units.RAMInBytes(memStr)
   155  		if err != nil {
   156  			return options, fmt.Errorf("failed to parse memory bytes %q: %w", memStr, err)
   157  		}
   158  	}
   159  	var memSwap64 int64
   160  	if memSwap != "" {
   161  		if memSwap == "-1" {
   162  			memSwap64 = -1
   163  		} else {
   164  			memSwap64, err = units.RAMInBytes(memSwap)
   165  			if err != nil {
   166  				return options, fmt.Errorf("failed to parse memory-swap bytes %q: %w", memSwap, err)
   167  			}
   168  			if mem64 > 0 && memSwap64 > 0 && memSwap64 < mem64 {
   169  				return options, fmt.Errorf("minimum memoryswap limit should be larger than memory limit, see usage")
   170  			}
   171  		}
   172  	} else {
   173  		memSwap64 = mem64 * 2
   174  	}
   175  	if memSwap64 == 0 {
   176  		memSwap64 = mem64 * 2
   177  	}
   178  	memReserve, err := cmd.Flags().GetString("memory-reservation")
   179  	if err != nil {
   180  		return options, err
   181  	}
   182  	var memReserve64 int64
   183  	if memReserve != "" {
   184  		memReserve64, err = units.RAMInBytes(memReserve)
   185  		if err != nil {
   186  			return options, fmt.Errorf("failed to parse memory bytes %q: %w", memReserve, err)
   187  		}
   188  	}
   189  	if mem64 > 0 && memReserve64 > 0 && mem64 < memReserve64 {
   190  		return options, fmt.Errorf("minimum memory limit can not be less than memory reservation limit, see usage")
   191  	}
   192  
   193  	kernelMemStr, err := cmd.Flags().GetString("kernel-memory")
   194  	if err != nil {
   195  		return options, err
   196  	}
   197  	if kernelMemStr != "" && cmd.Flag("kernel-memory").Changed {
   198  		log.L.Warnf("The --kernel-memory flag is no longer supported. This flag is a noop.")
   199  	}
   200  	cpuset, err := cmd.Flags().GetString("cpuset-cpus")
   201  	if err != nil {
   202  		return options, err
   203  	}
   204  	cpusetMems, err := cmd.Flags().GetString("cpuset-mems")
   205  	if err != nil {
   206  		return options, err
   207  	}
   208  	pidsLimit, err := cmd.Flags().GetInt64("pids-limit")
   209  	if err != nil {
   210  		return options, err
   211  	}
   212  	blkioWeight, err := cmd.Flags().GetUint16("blkio-weight")
   213  	if err != nil {
   214  		return options, err
   215  	}
   216  	if blkioWeight != 0 && !infoutil.BlockIOWeight(globalOptions.CgroupManager) {
   217  		return options, fmt.Errorf("kernel support for cgroup blkio weight missing, weight discarded")
   218  	}
   219  	if blkioWeight > 0 && blkioWeight < 10 || blkioWeight > 1000 {
   220  		return options, errors.New("range of blkio weight is from 10 to 1000")
   221  	}
   222  
   223  	if runtime.GOOS == "linux" {
   224  		options = updateResourceOptions{
   225  			CPUPeriod:          cpuPeriod,
   226  			CPUQuota:           cpuQuota,
   227  			CPUShares:          shares,
   228  			CpusetCpus:         cpuset,
   229  			CpusetMems:         cpusetMems,
   230  			MemoryLimitInBytes: mem64,
   231  			MemoryReservation:  memReserve64,
   232  			MemorySwapInBytes:  memSwap64,
   233  			PidsLimit:          pidsLimit,
   234  			BlkioWeight:        blkioWeight,
   235  		}
   236  	}
   237  	return options, nil
   238  }
   239  
   240  func updateContainer(ctx context.Context, client *containerd.Client, id string, opts updateResourceOptions, cmd *cobra.Command) error {
   241  	container, err := client.LoadContainer(ctx, id)
   242  	if err != nil {
   243  		return err
   244  	}
   245  	cStatus := formatter.ContainerStatus(ctx, container)
   246  	if cStatus == "pausing" {
   247  		return fmt.Errorf("container %q is in pausing state", id)
   248  	}
   249  	spec, err := container.Spec(ctx)
   250  	if err != nil {
   251  		return err
   252  	}
   253  
   254  	oldSpec, err := copySpec(spec)
   255  	if err != nil {
   256  		return err
   257  	}
   258  	if runtime.GOOS == "linux" {
   259  		if spec.Linux == nil {
   260  			spec.Linux = &runtimespec.Linux{}
   261  		}
   262  		if spec.Linux.Resources == nil {
   263  			spec.Linux.Resources = &runtimespec.LinuxResources{}
   264  		}
   265  		if spec.Linux.Resources.BlockIO == nil {
   266  			spec.Linux.Resources.BlockIO = &runtimespec.LinuxBlockIO{}
   267  		}
   268  		if cmd.Flags().Changed("blkio-weight") {
   269  			if spec.Linux.Resources.BlockIO.Weight != &opts.BlkioWeight {
   270  				spec.Linux.Resources.BlockIO.Weight = &opts.BlkioWeight
   271  			}
   272  		}
   273  		if spec.Linux.Resources.CPU == nil {
   274  			spec.Linux.Resources.CPU = &runtimespec.LinuxCPU{}
   275  		}
   276  		if cmd.Flags().Changed("cpu-shares") {
   277  			if spec.Linux.Resources.CPU.Shares != &opts.CPUShares {
   278  				spec.Linux.Resources.CPU.Shares = &opts.CPUShares
   279  			}
   280  		}
   281  		if cmd.Flags().Changed("cpu-quota") {
   282  			if spec.Linux.Resources.CPU.Quota != &opts.CPUQuota {
   283  				spec.Linux.Resources.CPU.Quota = &opts.CPUQuota
   284  			}
   285  		}
   286  		if cmd.Flags().Changed("cpu-period") {
   287  			if spec.Linux.Resources.CPU.Period != &opts.CPUPeriod {
   288  				spec.Linux.Resources.CPU.Period = &opts.CPUPeriod
   289  			}
   290  		}
   291  		if cmd.Flags().Changed("cpus") {
   292  			if spec.Linux.Resources.CPU.Cpus != opts.CpusetCpus {
   293  				spec.Linux.Resources.CPU.Cpus = opts.CpusetCpus
   294  			}
   295  		}
   296  		if cmd.Flags().Changed("cpuset-mems") {
   297  			if spec.Linux.Resources.CPU.Mems != opts.CpusetMems {
   298  				spec.Linux.Resources.CPU.Mems = opts.CpusetMems
   299  			}
   300  		}
   301  
   302  		if cmd.Flags().Changed("cpuset-cpus") {
   303  			if spec.Linux.Resources.CPU.Cpus != opts.CpusetCpus {
   304  				spec.Linux.Resources.CPU.Cpus = opts.CpusetCpus
   305  			}
   306  		}
   307  		if spec.Linux.Resources.Memory == nil {
   308  			spec.Linux.Resources.Memory = &runtimespec.LinuxMemory{}
   309  		}
   310  		if cmd.Flags().Changed("memory") {
   311  			if spec.Linux.Resources.Memory.Limit != &opts.MemoryLimitInBytes {
   312  				spec.Linux.Resources.Memory.Limit = &opts.MemoryLimitInBytes
   313  			}
   314  			if spec.Linux.Resources.Memory.Swap != &opts.MemorySwapInBytes {
   315  				spec.Linux.Resources.Memory.Swap = &opts.MemorySwapInBytes
   316  			}
   317  		}
   318  		if cmd.Flags().Changed("memory-reservation") {
   319  			if spec.Linux.Resources.Memory.Reservation != &opts.MemoryReservation {
   320  				spec.Linux.Resources.Memory.Reservation = &opts.MemoryReservation
   321  			}
   322  		}
   323  		if spec.Linux.Resources.Pids == nil {
   324  			spec.Linux.Resources.Pids = &runtimespec.LinuxPids{}
   325  		}
   326  		if cmd.Flags().Changed("pids-limit") {
   327  			if spec.Linux.Resources.Pids.Limit != opts.PidsLimit {
   328  				spec.Linux.Resources.Pids.Limit = opts.PidsLimit
   329  			}
   330  		}
   331  	}
   332  
   333  	if err := updateContainerSpec(ctx, container, spec); err != nil {
   334  		log.G(ctx).WithError(err).Errorf("Failed to update spec %+v for container %q", spec, id)
   335  		// reset spec on error.
   336  		if err := updateContainerSpec(ctx, container, oldSpec); err != nil {
   337  			log.G(ctx).WithError(err).Errorf("Failed to update spec %+v for container %q", oldSpec, id)
   338  		}
   339  	}
   340  
   341  	restart, err := cmd.Flags().GetString("restart")
   342  	if err != nil {
   343  		return err
   344  	}
   345  	if cmd.Flags().Changed("restart") && restart != "" {
   346  		if err := nerdctlContainer.UpdateContainerRestartPolicyLabel(ctx, client, container, restart); err != nil {
   347  			return err
   348  		}
   349  	}
   350  
   351  	// If container is not running, only update spec is enough, new resource
   352  	// limit will be applied when container start.
   353  	if cStatus != "Up" {
   354  		return nil
   355  	}
   356  	task, err := container.Task(ctx, nil)
   357  	if err != nil {
   358  		if errdefs.IsNotFound(err) {
   359  			// Task exited already.
   360  			return nil
   361  		}
   362  		return fmt.Errorf("failed to get task:%w", err)
   363  	}
   364  	return task.Update(ctx, containerd.WithResources(spec.Linux.Resources))
   365  }
   366  
   367  func updateContainerSpec(ctx context.Context, container containerd.Container, spec *runtimespec.Spec) error {
   368  	if err := container.Update(ctx, func(ctx context.Context, client *containerd.Client, c *containers.Container) error {
   369  		a, err := typeurl.MarshalAny(spec)
   370  		if err != nil {
   371  			return fmt.Errorf("failed to marshal spec %+v:%w", spec, err)
   372  		}
   373  		c.Spec = a
   374  		return nil
   375  	}); err != nil {
   376  		return fmt.Errorf("failed to update container spec:%w", err)
   377  	}
   378  	return nil
   379  }
   380  
   381  func copySpec(spec *runtimespec.Spec) (*runtimespec.Spec, error) {
   382  	var copySpec runtimespec.Spec
   383  	if err := util.DeepCopy(&copySpec, spec); err != nil {
   384  		return nil, fmt.Errorf("failed to deep copy:%w", err)
   385  	}
   386  	return &copySpec, nil
   387  }
   388  
   389  func updateShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   390  	return shellCompleteContainerNames(cmd, nil)
   391  }