diff options
49 files changed, 895 insertions, 314 deletions
diff --git a/cmd/podman/common/create.go b/cmd/podman/common/create.go index 1e573cc2d..a2bc45b9e 100644 --- a/cmd/podman/common/create.go +++ b/cmd/podman/common/create.go @@ -28,10 +28,10 @@ func ContainerToPodOptions(containerCreate *entities.ContainerCreateOptions, pod } // DefineCreateFlags declares and instantiates the container create flags -func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, isInfra bool, clone bool) { +func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, mode entities.ContainerMode) { createFlags := cmd.Flags() - if !isInfra && !clone { // regular create flags + if mode == entities.CreateMode { // regular create flags annotationFlagName := "annotation" createFlags.StringSliceVar( &cf.Annotation, @@ -103,22 +103,6 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, ) _ = cmd.RegisterFlagCompletionFunc(deviceCgroupRuleFlagName, completion.AutocompleteNone) - deviceReadIopsFlagName := "device-read-iops" - createFlags.StringSliceVar( - &cf.DeviceReadIOPs, - deviceReadIopsFlagName, []string{}, - "Limit read rate (IO per second) from a device (e.g. --device-read-iops=/dev/sda:1000)", - ) - _ = cmd.RegisterFlagCompletionFunc(deviceReadIopsFlagName, completion.AutocompleteDefault) - - deviceWriteIopsFlagName := "device-write-iops" - createFlags.StringSliceVar( - &cf.DeviceWriteIOPs, - deviceWriteIopsFlagName, []string{}, - "Limit write rate (IO per second) to a device (e.g. --device-write-iops=/dev/sda:1000)", - ) - _ = cmd.RegisterFlagCompletionFunc(deviceWriteIopsFlagName, completion.AutocompleteDefault) - createFlags.Bool( "disable-content-trust", false, "This is a Docker specific option and is a NOOP", @@ -597,7 +581,7 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, `If a container with the same name exists, replace it`, ) } - if isInfra || (!clone && !isInfra) { // infra container flags, create should also pick these up + if mode == entities.InfraMode || (mode == entities.CreateMode) { // infra container flags, create should also pick these up shmSizeFlagName := "shm-size" createFlags.String( shmSizeFlagName, shmSize(), @@ -677,7 +661,7 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, ) _ = cmd.RegisterFlagCompletionFunc(cgroupParentFlagName, completion.AutocompleteDefault) var conmonPidfileFlagName string - if !isInfra { + if mode == entities.CreateMode { conmonPidfileFlagName = "conmon-pidfile" } else { conmonPidfileFlagName = "infra-conmon-pidfile" @@ -690,7 +674,7 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, _ = cmd.RegisterFlagCompletionFunc(conmonPidfileFlagName, completion.AutocompleteDefault) var entrypointFlagName string - if !isInfra { + if mode == entities.CreateMode { entrypointFlagName = "entrypoint" } else { entrypointFlagName = "infra-command" @@ -725,7 +709,7 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, ) _ = cmd.RegisterFlagCompletionFunc(labelFileFlagName, completion.AutocompleteDefault) - if isInfra { + if mode == entities.InfraMode { nameFlagName := "infra-name" createFlags.StringVar( &cf.Name, @@ -775,7 +759,8 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, ) _ = cmd.RegisterFlagCompletionFunc(volumesFromFlagName, AutocompleteContainers) } - if clone || !isInfra { // clone and create only flags, we need this level of separation so clone does not pick up all of the flags + + if mode == entities.CloneMode || mode == entities.CreateMode { nameFlagName := "name" createFlags.StringVar( &cf.Name, @@ -791,7 +776,8 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, "Run container in an existing pod", ) _ = cmd.RegisterFlagCompletionFunc(podFlagName, AutocompletePods) - + } + if mode != entities.InfraMode { // clone create and update only flags, we need this level of separation so clone does not pick up all of the flags cpuPeriodFlagName := "cpu-period" createFlags.Uint64Var( &cf.CPUPeriod, @@ -840,8 +826,24 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, ) _ = cmd.RegisterFlagCompletionFunc(memorySwappinessFlagName, completion.AutocompleteNone) } - // anyone can use these + if mode == entities.CreateMode || mode == entities.UpdateMode { + deviceReadIopsFlagName := "device-read-iops" + createFlags.StringSliceVar( + &cf.DeviceReadIOPs, + deviceReadIopsFlagName, []string{}, + "Limit read rate (IO per second) from a device (e.g. --device-read-iops=/dev/sda:1000)", + ) + _ = cmd.RegisterFlagCompletionFunc(deviceReadIopsFlagName, completion.AutocompleteDefault) + deviceWriteIopsFlagName := "device-write-iops" + createFlags.StringSliceVar( + &cf.DeviceWriteIOPs, + deviceWriteIopsFlagName, []string{}, + "Limit write rate (IO per second) to a device (e.g. --device-write-iops=/dev/sda:1000)", + ) + _ = cmd.RegisterFlagCompletionFunc(deviceWriteIopsFlagName, completion.AutocompleteDefault) + } + // anyone can use these cpusFlagName := "cpus" createFlags.Float64Var( &cf.CPUS, diff --git a/cmd/podman/containers/clone.go b/cmd/podman/containers/clone.go index 9881a791c..62bc90a2c 100644 --- a/cmd/podman/containers/clone.go +++ b/cmd/podman/containers/clone.go @@ -41,7 +41,7 @@ func cloneFlags(cmd *cobra.Command) { flags.BoolVarP(&ctrClone.Force, forceFlagName, "f", false, "force the existing container to be destroyed") common.DefineCreateDefaults(&ctrClone.CreateOpts) - common.DefineCreateFlags(cmd, &ctrClone.CreateOpts, false, true) + common.DefineCreateFlags(cmd, &ctrClone.CreateOpts, entities.CloneMode) } func init() { registry.Commands = append(registry.Commands, registry.CliCommand{ diff --git a/cmd/podman/containers/create.go b/cmd/podman/containers/create.go index 455127fd7..b854ff4b2 100644 --- a/cmd/podman/containers/create.go +++ b/cmd/podman/containers/create.go @@ -72,7 +72,7 @@ func createFlags(cmd *cobra.Command) { flags.SetInterspersed(false) common.DefineCreateDefaults(&cliVals) - common.DefineCreateFlags(cmd, &cliVals, false, false) + common.DefineCreateFlags(cmd, &cliVals, entities.CreateMode) common.DefineNetFlags(cmd) flags.SetNormalizeFunc(utils.AliasFlags) diff --git a/cmd/podman/containers/run.go b/cmd/podman/containers/run.go index ef13ea95e..f66d4d4d3 100644 --- a/cmd/podman/containers/run.go +++ b/cmd/podman/containers/run.go @@ -61,7 +61,7 @@ func runFlags(cmd *cobra.Command) { flags.SetInterspersed(false) common.DefineCreateDefaults(&cliVals) - common.DefineCreateFlags(cmd, &cliVals, false, false) + common.DefineCreateFlags(cmd, &cliVals, entities.CreateMode) common.DefineNetFlags(cmd) flags.SetNormalizeFunc(utils.AliasFlags) diff --git a/cmd/podman/containers/update.go b/cmd/podman/containers/update.go new file mode 100644 index 000000000..33aa25f8e --- /dev/null +++ b/cmd/podman/containers/update.go @@ -0,0 +1,83 @@ +package containers + +import ( + "context" + "fmt" + + "github.com/containers/podman/v4/cmd/podman/common" + "github.com/containers/podman/v4/cmd/podman/registry" + "github.com/containers/podman/v4/pkg/domain/entities" + "github.com/containers/podman/v4/pkg/specgen" + "github.com/containers/podman/v4/pkg/specgenutil" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/spf13/cobra" +) + +var ( + updateDescription = `Updates the cgroup configuration of a given container` + + updateCommand = &cobra.Command{ + Use: "update [options] CONTAINER", + Short: "update an existing container", + Long: updateDescription, + RunE: update, + Args: cobra.ExactArgs(1), + ValidArgsFunction: common.AutocompleteContainers, + Example: `podman update --cpus=5 foobar_container`, + } + + containerUpdateCommand = &cobra.Command{ + Args: updateCommand.Args, + Use: updateCommand.Use, + Short: updateCommand.Short, + Long: updateCommand.Long, + RunE: updateCommand.RunE, + ValidArgsFunction: updateCommand.ValidArgsFunction, + Example: `podman container update --cpus=5 foobar_container`, + } +) +var ( + updateOpts entities.ContainerCreateOptions +) + +func updateFlags(cmd *cobra.Command) { + common.DefineCreateDefaults(&updateOpts) + common.DefineCreateFlags(cmd, &updateOpts, entities.UpdateMode) +} + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Command: updateCommand, + }) + updateFlags(updateCommand) + + registry.Commands = append(registry.Commands, registry.CliCommand{ + Command: containerUpdateCommand, + Parent: containerCmd, + }) + updateFlags(containerUpdateCommand) +} + +func update(cmd *cobra.Command, args []string) error { + var err error + // use a specgen since this is the easiest way to hold resource info + s := &specgen.SpecGenerator{} + s.ResourceLimits = &specs.LinuxResources{} + + // we need to pass the whole specgen since throttle devices are parsed later due to cross compat. + s.ResourceLimits, err = specgenutil.GetResources(s, &updateOpts) + if err != nil { + return err + } + + opts := &entities.ContainerUpdateOptions{ + NameOrID: args[0], + Specgen: s, + } + rep, err := registry.ContainerEngine().ContainerUpdate(context.Background(), opts) + if err != nil { + return err + } + fmt.Println(rep) + return nil +} diff --git a/cmd/podman/pods/clone.go b/cmd/podman/pods/clone.go index 9558c6aed..bf7d10c44 100644 --- a/cmd/podman/pods/clone.go +++ b/cmd/podman/pods/clone.go @@ -44,7 +44,7 @@ func cloneFlags(cmd *cobra.Command) { _ = podCloneCommand.RegisterFlagCompletionFunc(nameFlagName, completion.AutocompleteNone) common.DefineCreateDefaults(&podClone.InfraOptions) - common.DefineCreateFlags(cmd, &podClone.InfraOptions, true, false) + common.DefineCreateFlags(cmd, &podClone.InfraOptions, entities.InfraMode) podClone.InfraOptions.MemorySwappiness = -1 // this is not implemented for pods yet, need to set -1 default manually diff --git a/cmd/podman/pods/create.go b/cmd/podman/pods/create.go index 4f1f66ad6..d30f4782a 100644 --- a/cmd/podman/pods/create.go +++ b/cmd/podman/pods/create.go @@ -65,7 +65,7 @@ func init() { flags := createCommand.Flags() flags.SetInterspersed(false) common.DefineCreateDefaults(&infraOptions) - common.DefineCreateFlags(createCommand, &infraOptions, true, false) + common.DefineCreateFlags(createCommand, &infraOptions, entities.InfraMode) common.DefineNetFlags(createCommand) flags.BoolVar(&createOptions.Infra, "infra", true, "Create an infra container associated with the pod to share namespaces with") diff --git a/docs/source/markdown/.gitignore b/docs/source/markdown/.gitignore index 26509612d..74e7fc075 100644 --- a/docs/source/markdown/.gitignore +++ b/docs/source/markdown/.gitignore @@ -27,3 +27,4 @@ podman-run.1.md podman-search.1.md podman-stop.1.md podman-unpause.1.md +podman-update.1.md diff --git a/docs/source/markdown/links/podman-container-update.1 b/docs/source/markdown/links/podman-container-update.1 new file mode 100644 index 000000000..e903b5c06 --- /dev/null +++ b/docs/source/markdown/links/podman-container-update.1 @@ -0,0 +1 @@ +.so man1/podman-update.1 diff --git a/docs/source/markdown/options/device-read-bps.md b/docs/source/markdown/options/device-read-bps.md new file mode 100644 index 000000000..e0c610061 --- /dev/null +++ b/docs/source/markdown/options/device-read-bps.md @@ -0,0 +1,5 @@ +#### **--device-read-bps**=*path:rate* + +Limit read rate (in bytes per second) from a device (e.g. **--device-read-bps=/dev/sda:1mb**). + +This option is not supported on cgroups V1 rootless systems. diff --git a/docs/source/markdown/options/device-read-iops.md b/docs/source/markdown/options/device-read-iops.md new file mode 100644 index 000000000..9cd0f9030 --- /dev/null +++ b/docs/source/markdown/options/device-read-iops.md @@ -0,0 +1,5 @@ +#### **--device-read-iops**=*path:rate* + +Limit read rate (in IO operations per second) from a device (e.g. **--device-read-iops=/dev/sda:1000**). + +This option is not supported on cgroups V1 rootless systems. diff --git a/docs/source/markdown/options/device-write-bps.md b/docs/source/markdown/options/device-write-bps.md new file mode 100644 index 000000000..3dacc4515 --- /dev/null +++ b/docs/source/markdown/options/device-write-bps.md @@ -0,0 +1,5 @@ +#### **--device-write-bps**=*path:rate* + +Limit write rate (in bytes per second) to a device (e.g. **--device-write-bps=/dev/sda:1mb**). + +This option is not supported on cgroups V1 rootless systems. diff --git a/docs/source/markdown/options/device-write-iops.md b/docs/source/markdown/options/device-write-iops.md new file mode 100644 index 000000000..cf5ce3859 --- /dev/null +++ b/docs/source/markdown/options/device-write-iops.md @@ -0,0 +1,5 @@ +#### **--device-write-iops**=*path:rate* + +Limit write rate (in IO operations per second) to a device (e.g. **--device-write-iops=/dev/sda:1000**). + +This option is not supported on cgroups V1 rootless systems. diff --git a/docs/source/markdown/options/memory-reservation.md b/docs/source/markdown/options/memory-reservation.md new file mode 100644 index 000000000..410f1dd7c --- /dev/null +++ b/docs/source/markdown/options/memory-reservation.md @@ -0,0 +1,11 @@ +#### **--memory-reservation**=*number[unit]* + +Memory soft limit. A _unit_ can be **b** (bytes), **k** (kibibytes), **m** (mebibytes), or **g** (gibibytes). + +After setting memory reservation, when the system detects memory contention +or low memory, containers are forced to restrict their consumption to their +reservation. So you should always set the value below **--memory**, otherwise the +hard limit will take precedence. By default, memory reservation will be the same +as memory limit. + +This option is not supported on cgroups V1 rootless systems. diff --git a/docs/source/markdown/options/memory-swap.md b/docs/source/markdown/options/memory-swap.md new file mode 100644 index 000000000..08ee8b1a0 --- /dev/null +++ b/docs/source/markdown/options/memory-swap.md @@ -0,0 +1,13 @@ +#### **--memory-swap**=*number[unit]* + +A limit value equal to memory plus swap. +A _unit_ can be **b** (bytes), **k** (kibibytes), **m** (mebibytes), or **g** (gibibytes). + +Must be used with the **-m** (**--memory**) flag. +The argument value should always be larger than that of + **-m** (**--memory**) By default, it is set to double +the value of **--memory**. + +Set _number_ to **-1** to enable unlimited swap. + +This option is not supported on cgroups V1 rootless systems. diff --git a/docs/source/markdown/options/memory.md b/docs/source/markdown/options/memory.md new file mode 100644 index 000000000..1be9159c3 --- /dev/null +++ b/docs/source/markdown/options/memory.md @@ -0,0 +1,11 @@ +#### **--memory**, **-m**=*number[unit]* + +Memory limit. A _unit_ can be **b** (bytes), **k** (kibibytes), **m** (mebibytes), or **g** (gibibytes). + +Allows the memory available to a container to be constrained. If the host +supports swap memory, then the **-m** memory setting can be larger than physical +RAM. If a limit of 0 is specified (not using **-m**), the container's memory is +not limited. The actual limit may be rounded up to a multiple of the operating +system's page size (the value would be very large, that's millions of trillions). + +This option is not supported on cgroups V1 rootless systems. diff --git a/docs/source/markdown/options/name.container.md b/docs/source/markdown/options/name.container.md new file mode 100644 index 000000000..0c3df6ef1 --- /dev/null +++ b/docs/source/markdown/options/name.container.md @@ -0,0 +1,14 @@ +#### **--name**=*name* + +Assign a name to the container. + +The operator can identify a container in three ways: + +- UUID long identifier (“f78375b1c487e03c9438c729345e54db9d20cfa2ac1fc3494b6eb60872e74778”); +- UUID short identifier (“f78375b1c487”); +- Name (“jonah”). + +Podman generates a UUID for each container, and if a name is not assigned +to the container with **--name** then it will generate a random +string name. The name is useful any place you need to identify a container. +This works for both background and foreground containers. diff --git a/docs/source/markdown/podman-container-clone.1.md.in b/docs/source/markdown/podman-container-clone.1.md.in index 26f414b62..3e31389d2 100644 --- a/docs/source/markdown/podman-container-clone.1.md.in +++ b/docs/source/markdown/podman-container-clone.1.md.in @@ -52,36 +52,18 @@ If none are specified, the original container's CPU memory nodes are used. @@option destroy -#### **--device-read-bps**=*path* +@@option device-read-bps -Limit read rate (bytes per second) from a device (e.g. --device-read-bps=/dev/sda:1mb). - -This option is not supported on cgroups V1 rootless systems. - -#### **--device-write-bps**=*path* - -Limit write rate (bytes per second) to a device (e.g. --device-write-bps=/dev/sda:1mb) - -This option is not supported on cgroups V1 rootless systems. +@@option device-write-bps #### **--force**, **-f** Force removal of the original container that we are cloning. Can only be used in conjunction with **--destroy**. -#### **--memory**, **-m**=*limit* - -Memory limit (format: `<number>[<unit>]`, where unit = b (bytes), k (kibibytes), m (mebibytes), or g (gibibytes)) - -Allows the memory available to a container to be constrained. If the host -supports swap memory, then the **-m** memory setting can be larger than physical -RAM. If a limit of 0 is specified (not using **-m**), the container's memory is -not limited. The actual limit may be rounded up to a multiple of the operating -system's page size (the value would be very large, that's millions of trillions). +@@option memory If no memory limits are specified, the original container's will be used. -This option is not supported on cgroups V1 rootless systems. - #### **--memory-reservation**=*limit* Memory soft limit (format: `<number>[<unit>]`, where unit = b (bytes), k (kibibytes), m (mebibytes), or g (gibibytes)) @@ -92,8 +74,6 @@ reservation. So you should always set the value below **--memory**, otherwise th hard limit will take precedence. By default, memory reservation will be the same as memory limit from the container being cloned. -This option is not supported on cgroups V1 rootless systems. - #### **--memory-swap**=*limit* A limit value equal to memory plus swap. Must be used with the **-m** diff --git a/docs/source/markdown/podman-container.1.md b/docs/source/markdown/podman-container.1.md index a66e2789d..593cd3c42 100644 --- a/docs/source/markdown/podman-container.1.md +++ b/docs/source/markdown/podman-container.1.md @@ -46,6 +46,7 @@ The container command allows you to manage containers | top | [podman-top(1)](podman-top.1.md) | Display the running processes of a container. | | unmount | [podman-unmount(1)](podman-unmount.1.md) | Unmount a working container's root filesystem.(Alias unmount) | | unpause | [podman-unpause(1)](podman-unpause.1.md) | Unpause one or more containers. | +| update | [podman-update(1)](podman-update.1.md) | Updates the cgroup configuration of a given container. | | wait | [podman-wait(1)](podman-wait.1.md) | Wait on one or more containers to stop and print their exit codes. | ## SEE ALSO diff --git a/docs/source/markdown/podman-create.1.md.in b/docs/source/markdown/podman-create.1.md.in index 2fad2deb1..987f10181 100644 --- a/docs/source/markdown/podman-create.1.md.in +++ b/docs/source/markdown/podman-create.1.md.in @@ -87,9 +87,7 @@ each of stdin, stdout, and stderr. @@option blkio-weight -#### **--blkio-weight-device**=*device:weight* - -Block IO relative device weight. +@@option blkio-weight-device @@option cap-add @@ -148,27 +146,11 @@ device. The devices that podman will load modules when necessary are: #### **--device-read-bps**=*path* -Limit read rate (bytes per second) from a device (e.g. --device-read-bps=/dev/sda:1mb) - -This option is not supported on cgroups V1 rootless systems. - -#### **--device-read-iops**=*path* - -Limit read rate (IO per second) from a device (e.g. --device-read-iops=/dev/sda:1000) - -This option is not supported on cgroups V1 rootless systems. - -#### **--device-write-bps**=*path* +@@option device-read-iops -Limit write rate (bytes per second) to a device (e.g. --device-write-bps=/dev/sda:1mb) +@@option device-write-bps -This option is not supported on cgroups V1 rootless systems. - -#### **--device-write-iops**=*path* - -Limit write rate (IO per second) to a device (e.g. --device-write-iops=/dev/sda:1000) - -This option is not supported on cgroups V1 rootless systems. +@@option device-write-iops @@option disable-content-trust @@ -307,60 +289,17 @@ This option is currently supported only by the **journald** log driver. @@option mac-address -#### **--memory**, **-m**=*limit* - -Memory limit (format: `<number>[<unit>]`, where unit = b (bytes), k (kibibytes), m (mebibytes), or g (gibibytes)) - -Allows you to constrain the memory available to a container. If the host -supports swap memory, then the **-m** memory setting can be larger than physical -RAM. If a limit of 0 is specified (not using **-m**), the container's memory is -not limited. The actual limit may be rounded up to a multiple of the operating -system's page size (the value would be very large, that's millions of trillions). +@@option memory -This option is not supported on cgroups V1 rootless systems. +@@option memory-reservation -#### **--memory-reservation**=*limit* - -Memory soft limit (format: `<number>[<unit>]`, where unit = b (bytes), k (kibibytes), m (mebibytes), or g (gibibytes)) - -After setting memory reservation, when the system detects memory contention -or low memory, containers are forced to restrict their consumption to their -reservation. So you should always set the value below **--memory**, otherwise the -hard limit will take precedence. By default, memory reservation will be the same -as memory limit. - -This option is not supported on cgroups V1 rootless systems. - -#### **--memory-swap**=*limit* - -A limit value equal to memory plus swap. Must be used with the **-m** -(**--memory**) flag. The swap `LIMIT` should always be larger than **-m** -(**--memory**) value. By default, the swap `LIMIT` will be set to double -the value of --memory. - -The format of `LIMIT` is `<number>[<unit>]`. Unit can be `b` (bytes), -`k` (kibibytes), `m` (mebibytes), or `g` (gibibytes). If you don't specify a -unit, `b` is used. Set LIMIT to `-1` to enable unlimited swap. - -This option is not supported on cgroups V1 rootless systems. +@@option memory-swap @@option memory-swappiness @@option mount -#### **--name**=*name* - -Assign a name to the container - -The operator can identify a container in three ways: -UUID long identifier (“f78375b1c487e03c9438c729345e54db9d20cfa2ac1fc3494b6eb60872e74778”) -UUID short identifier (“f78375b1c487”) -Name (“jonah”) - -podman generates a UUID for each container, and if a name is not assigned -to the container with **--name** then it will generate a random -string name. The name is useful any place you need to identify a container. -This works for both background and foreground containers. +@@option name.container #### **--network**=*mode*, **--net** diff --git a/docs/source/markdown/podman-pod-clone.1.md.in b/docs/source/markdown/podman-pod-clone.1.md.in index 999297f5e..24edc44ec 100644 --- a/docs/source/markdown/podman-pod-clone.1.md.in +++ b/docs/source/markdown/podman-pod-clone.1.md.in @@ -48,13 +48,9 @@ Podman may load kernel modules required for using the specified device. The devices that Podman will load modules for when necessary are: /dev/fuse. -#### **--device-read-bps**=*path* +@@option device-read-bps -Limit read rate (bytes per second) from a device (e.g. --device-read-bps=/dev/sda:1mb). - -#### **--device-write-bps**=*path* - -Limit write rate (bytes per second) to a device (e.g. --device-write-bps=/dev/sda:1mb) +@@option device-write-bps @@option gidmap.pod diff --git a/docs/source/markdown/podman-pod-create.1.md.in b/docs/source/markdown/podman-pod-create.1.md.in index 2f8bcc31c..35d06fa00 100644 --- a/docs/source/markdown/podman-pod-create.1.md.in +++ b/docs/source/markdown/podman-pod-create.1.md.in @@ -65,13 +65,9 @@ Podman may load kernel modules required for using the specified device. The devices that Podman will load modules for when necessary are: /dev/fuse. -#### **--device-read-bps**=*path* +@@option device-read-bps -Limit read rate (bytes per second) from a device (e.g. --device-read-bps=/dev/sda:1mb) - -#### **--device-write-bps**=*path* - -Limit write rate (bytes per second) to a device (e.g. --device-write-bps=/dev/sda:1mb) +@@option device-write-bps #### **--dns**=*ipaddr* diff --git a/docs/source/markdown/podman-run.1.md.in b/docs/source/markdown/podman-run.1.md.in index c4df88e3b..64affa238 100644 --- a/docs/source/markdown/podman-run.1.md.in +++ b/docs/source/markdown/podman-run.1.md.in @@ -180,29 +180,13 @@ device. The devices that Podman will load modules when necessary are: @@option device-cgroup-rule -#### **--device-read-bps**=*path:rate* +@@option device-read-bps -Limit read rate (in bytes per second) from a device (e.g. **--device-read-bps=/dev/sda:1mb**). +@@option device-read-iops -This option is not supported on cgroups V1 rootless systems. +@@option device-write-bps -#### **--device-read-iops**=*path:rate* - -Limit read rate (in IO operations per second) from a device (e.g. **--device-read-iops=/dev/sda:1000**). - -This option is not supported on cgroups V1 rootless systems. - -#### **--device-write-bps**=*path:rate* - -Limit write rate (in bytes per second) to a device (e.g. **--device-write-bps=/dev/sda:1mb**). - -This option is not supported on cgroups V1 rootless systems. - -#### **--device-write-iops**=*path:rate* - -Limit write rate (in IO operations per second) to a device (e.g. **--device-write-iops=/dev/sda:1000**). - -This option is not supported on cgroups V1 rootless systems. +@@option device-write-iops @@option disable-content-trust @@ -335,52 +319,15 @@ RAM. If a limit of 0 is specified (not using **-m**), the container's memory is not limited. The actual limit may be rounded up to a multiple of the operating system's page size (the value would be very large, that's millions of trillions). -This option is not supported on cgroups V1 rootless systems. - -#### **--memory-reservation**=*number[unit]* - -Memory soft limit. A _unit_ can be **b** (bytes), **k** (kibibytes), **m** (mebibytes), or **g** (gibibytes). +@@option memory-reservation -After setting memory reservation, when the system detects memory contention -or low memory, containers are forced to restrict their consumption to their -reservation. So you should always set the value below **--memory**, otherwise the -hard limit will take precedence. By default, memory reservation will be the same -as memory limit. - -This option is not supported on cgroups V1 rootless systems. - -#### **--memory-swap**=*number[unit]* - -A limit value equal to memory plus swap. -A _unit_ can be **b** (bytes), **k** (kibibytes), **m** (mebibytes), or **g** (gibibytes). - -Must be used with the **-m** (**--memory**) flag. -The argument value should always be larger than that of - **-m** (**--memory**) By default, it is set to double -the value of **--memory**. - -Set _number_ to **-1** to enable unlimited swap. - -This option is not supported on cgroups V1 rootless systems. +@@option memory-swap @@option memory-swappiness @@option mount -#### **--name**=*name* - -Assign a name to the container. - -The operator can identify a container in three ways: - -- UUID long identifier (“f78375b1c487e03c9438c729345e54db9d20cfa2ac1fc3494b6eb60872e74778”); -- UUID short identifier (“f78375b1c487”); -- Name (“jonah”). - -Podman generates a UUID for each container, and if a name is not assigned -to the container with **--name** then it will generate a random -string name. The name is useful any place you need to identify a container. -This works for both background and foreground containers. +@@option name.container #### **--network**=*mode*, **--net** diff --git a/docs/source/markdown/podman-update.1.md.in b/docs/source/markdown/podman-update.1.md.in new file mode 100644 index 000000000..2928379f3 --- /dev/null +++ b/docs/source/markdown/podman-update.1.md.in @@ -0,0 +1,78 @@ +% podman-update(1) + +## NAME +podman\-update - Updates the cgroup configuration of a given container + +## SYNOPSIS +**podman update** [*options*] *container* + +**podman container update** [*options*] *container* + +## DESCRIPTION + +Updates the cgroup configuration of an already existing container. The currently supported options are a subset of the +podman create/run resource limits options. These new options are non-persistent and only last for the current execution of the container; the configuration will be honored on its next run. +This means that this command can only be executed on an already running container and the changes made will be erased the next time the container is stopped and restarted, this is to ensure immutability. +This command takes one argument, a container name or ID, alongside the resource flags to modify the cgroup. + +## OPTIONS + +@@option blkio-weight + +@@option blkio-weight-device + +@@option cpu-period + +@@option cpu-quota + +@@option cpu-rt-period + +@@option cpu-rt-runtime + +@@option cpu-shares + +@@option cpus.container + +@@option cpuset-cpus + +@@option cpuset-mems + +@@option device-read-bps + +@@option device-read-iops + +@@option device-write-bps + +@@option device-write-iops + +@@option memory + +@@option memory-reservation + +@@option memory-swap + +@@option memory-swappiness + + +## EXAMPLEs + +update a container with a new cpu quota and period +``` +podman update --cpus=5 myCtr +``` + +update a container with all available options for cgroups v2 +``` +podman update --cpus 5 --cpuset-cpus 0 --cpu-shares 123 --cpuset-mems 0 --memory 1G --memory-swap 2G --memory-reservation 2G --blkio-weight-device /dev/zero:123 --blkio-weight 123 --device-read-bps /dev/zero:10mb --device-write-bps /dev/zero:10mb --device-read-iops /dev/zero:1000 --device-write-iops /dev/zero:1000 ctrID +``` + +update a container with all available options for cgroups v1 +``` +podman update --cpus 5 --cpuset-cpus 0 --cpu-shares 123 --cpuset-mems 0 --memory 1G --memory-swap 2G --memory-reservation 2G --memory-swappiness 50 ctrID +``` + +## SEE ALSO +**[podman(1)](podman.1.md)**, **[podman-create(1)](podman-create.1.md)**, **[podman-run(1)](podman-run.1.md)** + +## HISTORY +August 2022, Originally written by Charlie Doern <cdoern@redhat.com> diff --git a/docs/source/markdown/podman.1.md b/docs/source/markdown/podman.1.md index d1192b6d2..8c3af2561 100644 --- a/docs/source/markdown/podman.1.md +++ b/docs/source/markdown/podman.1.md @@ -355,6 +355,7 @@ the exit codes follow the `chroot` standard, see below: | [podman-unpause(1)](podman-unpause.1.md) | Unpause one or more containers. | | [podman-unshare(1)](podman-unshare.1.md) | Run a command inside of a modified user namespace. | | [podman-untag(1)](podman-untag.1.md) | Removes one or more names from a locally-stored image. | +| [podman-update(1)](podman-update.1.md) | Updates the cgroup configuration of a given container. | | [podman-version(1)](podman-version.1.md) | Display the Podman version information. | | [podman-volume(1)](podman-volume.1.md) | Simple management tool for volumes. | | [podman-wait(1)](podman-wait.1.md) | Wait on one or more containers to stop and print their exit codes. | diff --git a/libpod/container_api.go b/libpod/container_api.go index 2ff4bfe08..f88e38ce1 100644 --- a/libpod/container_api.go +++ b/libpod/container_api.go @@ -16,6 +16,7 @@ import ( "github.com/containers/podman/v4/libpod/events" "github.com/containers/podman/v4/pkg/signal" "github.com/containers/storage/pkg/archive" + spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" ) @@ -98,6 +99,15 @@ func (c *Container) Start(ctx context.Context, recursive bool) error { return c.start() } +// Update updates the given container. +// only the cgroup config can be updated and therefore only a linux resource spec is passed. +func (c *Container) Update(res *spec.LinuxResources) error { + if err := c.syncContainer(); err != nil { + return err + } + return c.update(res) +} + // StartAndAttach starts a container and attaches to it. // This acts as a combination of the Start and Attach APIs, ensuring proper // ordering of the two such that no output from the container is lost (e.g. the diff --git a/libpod/container_internal.go b/libpod/container_internal.go index 7d390ec21..32674235a 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -2352,3 +2352,12 @@ func (c *Container) extractSecretToCtrStorage(secr *ContainerSecret) error { } return nil } + +// update calls the ociRuntime update function to modify a cgroup config after container creation +func (c *Container) update(resources *spec.LinuxResources) error { + if err := c.ociRuntime.UpdateContainer(c, resources); err != nil { + return err + } + logrus.Debugf("updated container %s", c.ID()) + return nil +} diff --git a/libpod/oci.go b/libpod/oci.go index 70053db1b..e5b9a0dcd 100644 --- a/libpod/oci.go +++ b/libpod/oci.go @@ -5,6 +5,7 @@ import ( "github.com/containers/common/pkg/resize" "github.com/containers/podman/v4/libpod/define" + "github.com/opencontainers/runtime-spec/specs-go" ) // OCIRuntime is an implementation of an OCI runtime. @@ -148,6 +149,9 @@ type OCIRuntime interface { // RuntimeInfo returns verbose information about the runtime. RuntimeInfo() (*define.ConmonInfo, *define.OCIRuntimeInfo, error) + + // UpdateContainer updates the given container's cgroup configuration. + UpdateContainer(ctr *Container, res *specs.LinuxResources) error } // AttachOptions are options used when attached to a container or an exec diff --git a/libpod/oci_conmon_common.go b/libpod/oci_conmon_common.go index b96f92d3a..cc65e1261 100644 --- a/libpod/oci_conmon_common.go +++ b/libpod/oci_conmon_common.go @@ -307,6 +307,52 @@ func (r *ConmonOCIRuntime) StartContainer(ctr *Container) error { return nil } +// UpdateContainer updates the given container's cgroup configuration +func (r *ConmonOCIRuntime) UpdateContainer(ctr *Container, resources *spec.LinuxResources) error { + runtimeDir, err := util.GetRuntimeDir() + if err != nil { + return err + } + env := []string{fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)} + if path, ok := os.LookupEnv("PATH"); ok { + env = append(env, fmt.Sprintf("PATH=%s", path)) + } + args := r.runtimeFlags + args = append(args, "update") + tempFile, additionalArgs, err := generateResourceFile(resources) + if err != nil { + return err + } + defer os.Remove(tempFile) + + args = append(args, additionalArgs...) + return utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, append(args, ctr.ID())...) +} + +func generateResourceFile(res *spec.LinuxResources) (string, []string, error) { + flags := []string{} + if res == nil { + return "", flags, nil + } + + f, err := ioutil.TempFile("", "podman") + if err != nil { + return "", nil, err + } + + j, err := json.Marshal(res) + if err != nil { + return "", nil, err + } + _, err = f.WriteString(string(j)) + if err != nil { + return "", nil, err + } + + flags = append(flags, "--resources="+f.Name()) + return f.Name(), flags, nil +} + // KillContainer sends the given signal to the given container. // If all is set, send to all PIDs in the container. // All is only supported if the container created cgroups. diff --git a/libpod/oci_missing.go b/libpod/oci_missing.go index 2ab2b4577..bbf2957ff 100644 --- a/libpod/oci_missing.go +++ b/libpod/oci_missing.go @@ -8,6 +8,7 @@ import ( "github.com/containers/common/pkg/resize" "github.com/containers/podman/v4/libpod/define" + spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" ) @@ -80,6 +81,11 @@ func (r *MissingRuntime) StartContainer(ctr *Container) error { return r.printError() } +// UpdateContainer is not available as the runtime is missing +func (r *MissingRuntime) UpdateContainer(ctr *Container, resources *spec.LinuxResources) error { + return r.printError() +} + // KillContainer is not available as the runtime is missing // TODO: We could attempt to unix.Kill() the PID as recorded in the state if we // really want to smooth things out? Won't be perfect, but if the container has diff --git a/pkg/api/handlers/libpod/containers.go b/pkg/api/handlers/libpod/containers.go index 5d85d4009..d1460569f 100644 --- a/pkg/api/handlers/libpod/containers.go +++ b/pkg/api/handlers/libpod/containers.go @@ -1,6 +1,7 @@ package libpod import ( + "encoding/json" "errors" "fmt" "io/ioutil" @@ -10,6 +11,7 @@ import ( "github.com/containers/podman/v4/libpod" "github.com/containers/podman/v4/libpod/define" + "github.com/containers/podman/v4/pkg/api/handlers" "github.com/containers/podman/v4/pkg/api/handlers/compat" "github.com/containers/podman/v4/pkg/api/handlers/utils" api "github.com/containers/podman/v4/pkg/api/types" @@ -17,6 +19,7 @@ import ( "github.com/containers/podman/v4/pkg/domain/infra/abi" "github.com/containers/podman/v4/pkg/util" "github.com/gorilla/schema" + "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" ) @@ -391,6 +394,28 @@ func InitContainer(w http.ResponseWriter, r *http.Request) { utils.WriteResponse(w, http.StatusNoContent, "") } +func UpdateContainer(w http.ResponseWriter, r *http.Request) { + name := utils.GetName(r) + runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) + ctr, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + + options := &handlers.UpdateEntities{Resources: &specs.LinuxResources{}} + if err := json.NewDecoder(r.Body).Decode(&options.Resources); err != nil { + utils.Error(w, http.StatusInternalServerError, fmt.Errorf("decode(): %w", err)) + return + } + err = ctr.Update(options.Resources) + if err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusCreated, ctr.ID()) +} + func ShouldRestart(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) // Now use the ABI implementation to prevent us from having duplicate diff --git a/pkg/api/handlers/swagger/responses.go b/pkg/api/handlers/swagger/responses.go index 93a508b39..3de9b06e9 100644 --- a/pkg/api/handlers/swagger/responses.go +++ b/pkg/api/handlers/swagger/responses.go @@ -313,6 +313,11 @@ type containerCreateResponse struct { Body entities.ContainerCreateResponse } +type containerUpdateResponse struct { + // in:body + ID string +} + // Wait container // swagger:response type containerWaitResponse struct { diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go index aab905878..bb416d9f4 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -11,6 +11,7 @@ import ( dockerContainer "github.com/docker/docker/api/types/container" dockerNetwork "github.com/docker/docker/api/types/network" "github.com/docker/go-connections/nat" + "github.com/opencontainers/runtime-spec/specs-go" ) type AuthConfig struct { @@ -64,6 +65,12 @@ type LibpodContainersRmReport struct { RmError string `json:"Err,omitempty"` } +// UpdateEntities used to wrap the oci resource spec in a swagger model +// swagger:model +type UpdateEntities struct { + Resources *specs.LinuxResources +} + type Info struct { docker.Info BuildahVersion string diff --git a/pkg/api/server/register_containers.go b/pkg/api/server/register_containers.go index 8aba4ea05..41baf5418 100644 --- a/pkg/api/server/register_containers.go +++ b/pkg/api/server/register_containers.go @@ -1626,5 +1626,33 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // 500: // $ref: "#/responses/internalError" r.HandleFunc(VersionedPath("/libpod/containers/{name}/rename"), s.APIHandler(compat.RenameContainer)).Methods(http.MethodPost) + // swagger:operation POST /libpod/containers/{name}/update libpod ContainerUpdateLibpod + // --- + // tags: + // - containers + // summary: Update an existing containers cgroup configuration + // description: Update an existing containers cgroup configuration. + // parameters: + // - in: path + // name: name + // type: string + // required: true + // description: Full or partial ID or full name of the container to update + // - in: body + // name: resources + // description: attributes for updating the container + // schema: + // $ref: "#/definitions/UpdateEntities" + // produces: + // - application/json + // responses: + // responses: + // 201: + // $ref: "#/responses/containerUpdateResponse" + // 404: + // $ref: "#/responses/containerNotFound" + // 500: + // $ref: "#/responses/internalError" + r.HandleFunc(VersionedPath("/libpod/containers/{name}/update"), s.APIHandler(libpod.UpdateContainer)).Methods(http.MethodPost) return nil } diff --git a/pkg/bindings/containers/update.go b/pkg/bindings/containers/update.go new file mode 100644 index 000000000..7cda7c306 --- /dev/null +++ b/pkg/bindings/containers/update.go @@ -0,0 +1,31 @@ +package containers + +import ( + "context" + "net/http" + "strings" + + "github.com/containers/podman/v4/pkg/bindings" + "github.com/containers/podman/v4/pkg/domain/entities" + jsoniter "github.com/json-iterator/go" +) + +func Update(ctx context.Context, options *entities.ContainerUpdateOptions) (string, error) { + conn, err := bindings.GetClient(ctx) + if err != nil { + return "", err + } + + resources, err := jsoniter.MarshalToString(options.Specgen.ResourceLimits) + if err != nil { + return "", err + } + stringReader := strings.NewReader(resources) + response, err := conn.DoRequest(ctx, stringReader, http.MethodPost, "/containers/%s/update", nil, nil, options.NameOrID) + if err != nil { + return "", err + } + defer response.Body.Close() + + return options.NameOrID, response.Process(nil) +} diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go index 91ccdc2b2..47225f25c 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -495,3 +495,9 @@ type ContainerCloneOptions struct { Run bool Force bool } + +// ContainerUpdateOptions containers options for updating an existing containers cgroup configuration +type ContainerUpdateOptions struct { + NameOrID string + Specgen *specgen.SpecGenerator +} diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index 6a766eb84..69adc9732 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -51,6 +51,7 @@ type ContainerEngine interface { ContainerTop(ctx context.Context, options TopOptions) (*StringSliceReport, error) ContainerUnmount(ctx context.Context, nameOrIDs []string, options ContainerUnmountOptions) ([]*ContainerUnmountReport, error) ContainerUnpause(ctx context.Context, namesOrIds []string, options PauseUnPauseOptions) ([]*PauseUnpauseReport, error) + ContainerUpdate(ctx context.Context, options *ContainerUpdateOptions) (string, error) ContainerWait(ctx context.Context, namesOrIds []string, options WaitOptions) ([]WaitReport, error) Diff(ctx context.Context, namesOrIds []string, options DiffOptions) (*DiffReport, error) Events(ctx context.Context, opts EventsOptions) error diff --git a/pkg/domain/entities/pods.go b/pkg/domain/entities/pods.go index 33ca2c807..b672434d8 100644 --- a/pkg/domain/entities/pods.go +++ b/pkg/domain/entities/pods.go @@ -164,6 +164,15 @@ type PodCloneOptions struct { Start bool } +type ContainerMode string + +const ( + InfraMode = ContainerMode("infra") + CloneMode = ContainerMode("clone") + UpdateMode = ContainerMode("update") + CreateMode = ContainerMode("create") +) + type ContainerCreateOptions struct { Annotation []string Attach []string diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index 0a8e5bc2f..dfa3c5ba0 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -1715,3 +1715,27 @@ func (ic *ContainerEngine) ContainerClone(ctx context.Context, ctrCloneOpts enti return &entities.ContainerCreateReport{Id: ctr.ID()}, nil } + +// ContainerUpdate finds and updates the given container's cgroup config with the specified options +func (ic *ContainerEngine) ContainerUpdate(ctx context.Context, updateOptions *entities.ContainerUpdateOptions) (string, error) { + err := specgen.WeightDevices(updateOptions.Specgen) + if err != nil { + return "", err + } + err = specgen.FinishThrottleDevices(updateOptions.Specgen) + if err != nil { + return "", err + } + ctrs, err := getContainersByContext(false, false, []string{updateOptions.NameOrID}, ic.Libpod) + if err != nil { + return "", err + } + if len(ctrs) != 1 { + return "", fmt.Errorf("container not found") + } + + if err = ctrs[0].Update(updateOptions.Specgen.ResourceLimits); err != nil { + return "", err + } + return ctrs[0].ID(), nil +} diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index 023bee430..68ca788b8 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -1024,3 +1024,16 @@ func (ic *ContainerEngine) ContainerRename(ctx context.Context, nameOrID string, func (ic *ContainerEngine) ContainerClone(ctx context.Context, ctrCloneOpts entities.ContainerCloneOptions) (*entities.ContainerCreateReport, error) { return nil, errors.New("cloning a container is not supported on the remote client") } + +// ContainerUpdate finds and updates the given container's cgroup config with the specified options +func (ic *ContainerEngine) ContainerUpdate(ctx context.Context, updateOptions *entities.ContainerUpdateOptions) (string, error) { + err := specgen.WeightDevices(updateOptions.Specgen) + if err != nil { + return "", err + } + err = specgen.FinishThrottleDevices(updateOptions.Specgen) + if err != nil { + return "", err + } + return containers.Update(ic.ClientCtx, updateOptions) +} diff --git a/pkg/specgen/generate/container.go b/pkg/specgen/generate/container.go index d57efa0d1..46b7a2dc2 100644 --- a/pkg/specgen/generate/container.go +++ b/pkg/specgen/generate/container.go @@ -21,7 +21,6 @@ import ( spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/openshift/imagebuilder" "github.com/sirupsen/logrus" - "golang.org/x/sys/unix" ) func getImageFromSpec(ctx context.Context, r *libpod.Runtime, s *specgen.SpecGenerator) (*libimage.Image, string, *libimage.ImageData, error) { @@ -518,75 +517,6 @@ func mapSecurityConfig(c *libpod.ContainerConfig, s *specgen.SpecGenerator) { s.HostUsers = c.HostUsers } -// FinishThrottleDevices takes the temporary representation of the throttle -// devices in the specgen and looks up the major and major minors. it then -// sets the throttle devices proper in the specgen -func FinishThrottleDevices(s *specgen.SpecGenerator) error { - if s.ResourceLimits == nil { - s.ResourceLimits = &spec.LinuxResources{} - } - if bps := s.ThrottleReadBpsDevice; len(bps) > 0 { - if s.ResourceLimits.BlockIO == nil { - s.ResourceLimits.BlockIO = &spec.LinuxBlockIO{} - } - for k, v := range bps { - statT := unix.Stat_t{} - if err := unix.Stat(k, &statT); err != nil { - return fmt.Errorf("could not parse throttle device at %s: %w", k, err) - } - v.Major = (int64(unix.Major(uint64(statT.Rdev)))) //nolint: unconvert - v.Minor = (int64(unix.Minor(uint64(statT.Rdev)))) //nolint: unconvert - if s.ResourceLimits.BlockIO == nil { - s.ResourceLimits.BlockIO = new(spec.LinuxBlockIO) - } - s.ResourceLimits.BlockIO.ThrottleReadBpsDevice = append(s.ResourceLimits.BlockIO.ThrottleReadBpsDevice, v) - } - } - if bps := s.ThrottleWriteBpsDevice; len(bps) > 0 { - if s.ResourceLimits.BlockIO == nil { - s.ResourceLimits.BlockIO = &spec.LinuxBlockIO{} - } - for k, v := range bps { - statT := unix.Stat_t{} - if err := unix.Stat(k, &statT); err != nil { - return fmt.Errorf("could not parse throttle device at %s: %w", k, err) - } - v.Major = (int64(unix.Major(uint64(statT.Rdev)))) //nolint: unconvert - v.Minor = (int64(unix.Minor(uint64(statT.Rdev)))) //nolint: unconvert - s.ResourceLimits.BlockIO.ThrottleWriteBpsDevice = append(s.ResourceLimits.BlockIO.ThrottleWriteBpsDevice, v) - } - } - if iops := s.ThrottleReadIOPSDevice; len(iops) > 0 { - if s.ResourceLimits.BlockIO == nil { - s.ResourceLimits.BlockIO = &spec.LinuxBlockIO{} - } - for k, v := range iops { - statT := unix.Stat_t{} - if err := unix.Stat(k, &statT); err != nil { - return fmt.Errorf("could not parse throttle device at %s: %w", k, err) - } - v.Major = (int64(unix.Major(uint64(statT.Rdev)))) //nolint: unconvert - v.Minor = (int64(unix.Minor(uint64(statT.Rdev)))) //nolint: unconvert - s.ResourceLimits.BlockIO.ThrottleReadIOPSDevice = append(s.ResourceLimits.BlockIO.ThrottleReadIOPSDevice, v) - } - } - if iops := s.ThrottleWriteIOPSDevice; len(iops) > 0 { - if s.ResourceLimits.BlockIO == nil { - s.ResourceLimits.BlockIO = &spec.LinuxBlockIO{} - } - for k, v := range iops { - statT := unix.Stat_t{} - if err := unix.Stat(k, &statT); err != nil { - return fmt.Errorf("could not parse throttle device at %s: %w", k, err) - } - v.Major = (int64(unix.Major(uint64(statT.Rdev)))) //nolint: unconvert - v.Minor = (int64(unix.Minor(uint64(statT.Rdev)))) //nolint: unconvert - s.ResourceLimits.BlockIO.ThrottleWriteIOPSDevice = append(s.ResourceLimits.BlockIO.ThrottleWriteIOPSDevice, v) - } - } - return nil -} - // Check name looks for existing containers/pods with the same name, and modifies the given string until a new name is found func CheckName(rt *libpod.Runtime, n string, kind bool) string { switch { diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go index 819800176..4d5ac22ad 100644 --- a/pkg/specgen/generate/container_create.go +++ b/pkg/specgen/generate/container_create.go @@ -56,7 +56,7 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener } } - if err := FinishThrottleDevices(s); err != nil { + if err := specgen.FinishThrottleDevices(s); err != nil { return nil, nil, nil, err } diff --git a/pkg/specgen/generate/pod_create.go b/pkg/specgen/generate/pod_create.go index d6063b9a0..14d390e49 100644 --- a/pkg/specgen/generate/pod_create.go +++ b/pkg/specgen/generate/pod_create.go @@ -45,7 +45,7 @@ func MakePod(p *entities.PodSpec, rt *libpod.Runtime) (*libpod.Pod, error) { } if !p.PodSpecGen.NoInfra { - err := FinishThrottleDevices(p.PodSpecGen.InfraContainerSpec) + err := specgen.FinishThrottleDevices(p.PodSpecGen.InfraContainerSpec) if err != nil { return nil, err } @@ -53,17 +53,11 @@ func MakePod(p *entities.PodSpec, rt *libpod.Runtime) (*libpod.Pod, error) { p.PodSpecGen.ResourceLimits.BlockIO = p.PodSpecGen.InfraContainerSpec.ResourceLimits.BlockIO } - weightDevices, err := WeightDevices(p.PodSpecGen.InfraContainerSpec.WeightDevice) + err = specgen.WeightDevices(p.PodSpecGen.InfraContainerSpec) if err != nil { return nil, err } - - if p.PodSpecGen.ResourceLimits != nil && len(weightDevices) > 0 { - if p.PodSpecGen.ResourceLimits.BlockIO == nil { - p.PodSpecGen.ResourceLimits.BlockIO = &specs.LinuxBlockIO{} - } - p.PodSpecGen.ResourceLimits.BlockIO.WeightDevice = weightDevices - } + p.PodSpecGen.ResourceLimits = p.PodSpecGen.InfraContainerSpec.ResourceLimits } options, err := createPodOptions(&p.PodSpecGen) diff --git a/pkg/specgen/utils.go b/pkg/specgen/utils.go new file mode 100644 index 000000000..dc9127bb3 --- /dev/null +++ b/pkg/specgen/utils.go @@ -0,0 +1,14 @@ +//go:build !linux +// +build !linux + +package specgen + +// FinishThrottleDevices cannot be called on non-linux OS' due to importing unix functions +func FinishThrottleDevices(s *SpecGenerator) error { + return nil +} + +// WeightDevices cannot be called on non-linux OS' due to importing unix functions +func WeightDevices(s *SpecGenerator) error { + return nil +} diff --git a/pkg/specgen/utils_linux.go b/pkg/specgen/utils_linux.go new file mode 100644 index 000000000..d8e4cbae3 --- /dev/null +++ b/pkg/specgen/utils_linux.go @@ -0,0 +1,103 @@ +//go:build linux +// +build linux + +package specgen + +import ( + "fmt" + + spec "github.com/opencontainers/runtime-spec/specs-go" + "golang.org/x/sys/unix" +) + +// FinishThrottleDevices takes the temporary representation of the throttle +// devices in the specgen and looks up the major and major minors. it then +// sets the throttle devices proper in the specgen +func FinishThrottleDevices(s *SpecGenerator) error { + if s.ResourceLimits == nil { + s.ResourceLimits = &spec.LinuxResources{} + } + if bps := s.ThrottleReadBpsDevice; len(bps) > 0 { + if s.ResourceLimits.BlockIO == nil { + s.ResourceLimits.BlockIO = &spec.LinuxBlockIO{} + } + for k, v := range bps { + statT := unix.Stat_t{} + if err := unix.Stat(k, &statT); err != nil { + return fmt.Errorf("could not parse throttle device at %s: %w", k, err) + } + v.Major = (int64(unix.Major(uint64(statT.Rdev)))) //nolint: unconvert + v.Minor = (int64(unix.Minor(uint64(statT.Rdev)))) //nolint: unconvert + if s.ResourceLimits.BlockIO == nil { + s.ResourceLimits.BlockIO = new(spec.LinuxBlockIO) + } + s.ResourceLimits.BlockIO.ThrottleReadBpsDevice = append(s.ResourceLimits.BlockIO.ThrottleReadBpsDevice, v) + } + } + if bps := s.ThrottleWriteBpsDevice; len(bps) > 0 { + if s.ResourceLimits.BlockIO == nil { + s.ResourceLimits.BlockIO = &spec.LinuxBlockIO{} + } + for k, v := range bps { + statT := unix.Stat_t{} + if err := unix.Stat(k, &statT); err != nil { + return fmt.Errorf("could not parse throttle device at %s: %w", k, err) + } + v.Major = (int64(unix.Major(uint64(statT.Rdev)))) //nolint: unconvert + v.Minor = (int64(unix.Minor(uint64(statT.Rdev)))) //nolint: unconvert + s.ResourceLimits.BlockIO.ThrottleWriteBpsDevice = append(s.ResourceLimits.BlockIO.ThrottleWriteBpsDevice, v) + } + } + if iops := s.ThrottleReadIOPSDevice; len(iops) > 0 { + if s.ResourceLimits.BlockIO == nil { + s.ResourceLimits.BlockIO = &spec.LinuxBlockIO{} + } + for k, v := range iops { + statT := unix.Stat_t{} + if err := unix.Stat(k, &statT); err != nil { + return fmt.Errorf("could not parse throttle device at %s: %w", k, err) + } + v.Major = (int64(unix.Major(uint64(statT.Rdev)))) //nolint: unconvert + v.Minor = (int64(unix.Minor(uint64(statT.Rdev)))) //nolint: unconvert + s.ResourceLimits.BlockIO.ThrottleReadIOPSDevice = append(s.ResourceLimits.BlockIO.ThrottleReadIOPSDevice, v) + } + } + if iops := s.ThrottleWriteIOPSDevice; len(iops) > 0 { + if s.ResourceLimits.BlockIO == nil { + s.ResourceLimits.BlockIO = &spec.LinuxBlockIO{} + } + for k, v := range iops { + statT := unix.Stat_t{} + if err := unix.Stat(k, &statT); err != nil { + return fmt.Errorf("could not parse throttle device at %s: %w", k, err) + } + v.Major = (int64(unix.Major(uint64(statT.Rdev)))) //nolint: unconvert + v.Minor = (int64(unix.Minor(uint64(statT.Rdev)))) //nolint: unconvert + s.ResourceLimits.BlockIO.ThrottleWriteIOPSDevice = append(s.ResourceLimits.BlockIO.ThrottleWriteIOPSDevice, v) + } + } + return nil +} + +func WeightDevices(specgen *SpecGenerator) error { + devs := []spec.LinuxWeightDevice{} + if specgen.ResourceLimits == nil { + specgen.ResourceLimits = &spec.LinuxResources{} + } + for k, v := range specgen.WeightDevice { + statT := unix.Stat_t{} + if err := unix.Stat(k, &statT); err != nil { + return fmt.Errorf("failed to inspect '%s' in --blkio-weight-device: %w", k, err) + } + dev := new(spec.LinuxWeightDevice) + dev.Major = (int64(unix.Major(uint64(statT.Rdev)))) //nolint: unconvert + dev.Minor = (int64(unix.Minor(uint64(statT.Rdev)))) //nolint: unconvert + dev.Weight = v.Weight + devs = append(devs, *dev) + if specgen.ResourceLimits.BlockIO == nil { + specgen.ResourceLimits.BlockIO = &spec.LinuxBlockIO{} + } + specgen.ResourceLimits.BlockIO.WeightDevice = devs + } + return nil +} diff --git a/pkg/specgenutil/specgen.go b/pkg/specgenutil/specgen.go index d0e09fe72..439a13385 100644 --- a/pkg/specgenutil/specgen.go +++ b/pkg/specgenutil/specgen.go @@ -507,44 +507,9 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions s.ResourceLimits = &specs.LinuxResources{} } - if s.ResourceLimits.Memory == nil || (len(c.Memory) != 0 || len(c.MemoryReservation) != 0 || len(c.MemorySwap) != 0 || c.MemorySwappiness != 0) { - s.ResourceLimits.Memory, err = getMemoryLimits(c) - if err != nil { - return err - } - } - if s.ResourceLimits.BlockIO == nil || (len(c.BlkIOWeight) != 0 || len(c.BlkIOWeightDevice) != 0 || len(c.DeviceReadBPs) != 0 || len(c.DeviceWriteBPs) != 0) { - s.ResourceLimits.BlockIO, err = getIOLimits(s, c) - if err != nil { - return err - } - } - if c.PIDsLimit != nil { - pids := specs.LinuxPids{ - Limit: *c.PIDsLimit, - } - - s.ResourceLimits.Pids = &pids - } - - if s.ResourceLimits.CPU == nil || (c.CPUPeriod != 0 || c.CPUQuota != 0 || c.CPURTPeriod != 0 || c.CPURTRuntime != 0 || c.CPUS != 0 || len(c.CPUSetCPUs) != 0 || len(c.CPUSetMems) != 0 || c.CPUShares != 0) { - s.ResourceLimits.CPU = getCPULimits(c) - } - - unifieds := make(map[string]string) - for _, unified := range c.CgroupConf { - splitUnified := strings.SplitN(unified, "=", 2) - if len(splitUnified) < 2 { - return errors.New("--cgroup-conf must be formatted KEY=VALUE") - } - unifieds[splitUnified[0]] = splitUnified[1] - } - if len(unifieds) > 0 { - s.ResourceLimits.Unified = unifieds - } - - if s.ResourceLimits.CPU == nil && s.ResourceLimits.Pids == nil && s.ResourceLimits.BlockIO == nil && s.ResourceLimits.Memory == nil && s.ResourceLimits.Unified == nil { - s.ResourceLimits = nil + s.ResourceLimits, err = GetResources(s, c) + if err != nil { + return err } if s.LogConfiguration == nil { @@ -1171,3 +1136,47 @@ func parseLinuxResourcesDeviceAccess(device string) (specs.LinuxDeviceCgroup, er Access: access, }, nil } + +func GetResources(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions) (*specs.LinuxResources, error) { + var err error + if s.ResourceLimits.Memory == nil || (len(c.Memory) != 0 || len(c.MemoryReservation) != 0 || len(c.MemorySwap) != 0 || c.MemorySwappiness != 0) { + s.ResourceLimits.Memory, err = getMemoryLimits(c) + if err != nil { + return nil, err + } + } + if s.ResourceLimits.BlockIO == nil || (len(c.BlkIOWeight) != 0 || len(c.BlkIOWeightDevice) != 0 || len(c.DeviceReadBPs) != 0 || len(c.DeviceWriteBPs) != 0) { + s.ResourceLimits.BlockIO, err = getIOLimits(s, c) + if err != nil { + return nil, err + } + } + if c.PIDsLimit != nil { + pids := specs.LinuxPids{ + Limit: *c.PIDsLimit, + } + + s.ResourceLimits.Pids = &pids + } + + if s.ResourceLimits.CPU == nil || (c.CPUPeriod != 0 || c.CPUQuota != 0 || c.CPURTPeriod != 0 || c.CPURTRuntime != 0 || c.CPUS != 0 || len(c.CPUSetCPUs) != 0 || len(c.CPUSetMems) != 0 || c.CPUShares != 0) { + s.ResourceLimits.CPU = getCPULimits(c) + } + + unifieds := make(map[string]string) + for _, unified := range c.CgroupConf { + splitUnified := strings.SplitN(unified, "=", 2) + if len(splitUnified) < 2 { + return nil, errors.New("--cgroup-conf must be formatted KEY=VALUE") + } + unifieds[splitUnified[0]] = splitUnified[1] + } + if len(unifieds) > 0 { + s.ResourceLimits.Unified = unifieds + } + + if s.ResourceLimits.CPU == nil && s.ResourceLimits.Pids == nil && s.ResourceLimits.BlockIO == nil && s.ResourceLimits.Memory == nil && s.ResourceLimits.Unified == nil { + s.ResourceLimits = nil + } + return s.ResourceLimits, nil +} diff --git a/test/apiv2/20-containers.at b/test/apiv2/20-containers.at index 253bb2e45..9ace46b8b 100644 --- a/test/apiv2/20-containers.at +++ b/test/apiv2/20-containers.at @@ -547,6 +547,21 @@ t GET libpod/containers/$cname/json 200 \ .ImageName=$IMAGE \ .Name=$cname +if root; then + podman run -dt --name=updateCtr alpine + echo '{"Memory":{"Limit":500000}, "CPU":{"Shares":123}}' >${TMPD}/update.json + t POST libpod/containers/updateCtr/update ${TMPD}/update.json 201 + + # Verify + echo '{ "AttachStdout":true,"Cmd":["cat","/sys/fs/cgroup/cpu.weight"]}' >${TMPD}/exec.json + t POST containers/updateCtr/exec ${TMPD}/exec.json 201 .Id~[0-9a-f]\\{64\\} + eid=$(jq -r '.Id' <<<"$output") + # 002 is the byte length + t POST exec/$eid/start 200 $'\001\0025' + + podman rm -f updateCtr +fi + rm -rf $TMPD podman container rm -fa diff --git a/test/apiv2/python/rest_api/test_v2_0_0_container.py b/test/apiv2/python/rest_api/test_v2_0_0_container.py index a6cd93a1a..25596a9b7 100644 --- a/test/apiv2/python/rest_api/test_v2_0_0_container.py +++ b/test/apiv2/python/rest_api/test_v2_0_0_container.py @@ -359,8 +359,6 @@ class ContainerTestCase(APITestCase): self.assertEqual(2000, out["HostConfig"]["MemorySwap"]) self.assertEqual(1000, out["HostConfig"]["Memory"]) - - def execute_process(cmd): return subprocess.run( cmd, diff --git a/test/e2e/update_test.go b/test/e2e/update_test.go new file mode 100644 index 000000000..97dadd04c --- /dev/null +++ b/test/e2e/update_test.go @@ -0,0 +1,200 @@ +package integration + +import ( + "github.com/containers/common/pkg/cgroupv2" + . "github.com/containers/podman/v4/test/utils" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Podman update", func() { + var ( + tempdir string + err error + podmanTest *PodmanTestIntegration + ) + + BeforeEach(func() { + tempdir, err = CreateTempDirInTempDir() + Expect(err).To(BeNil()) + podmanTest = PodmanTestCreate(tempdir) + podmanTest.Setup() + }) + + AfterEach(func() { + podmanTest.Cleanup() + f := CurrentGinkgoTestDescription() + processTestResult(f) + + }) + + It("podman update container all options v1", func() { + SkipIfCgroupV2("testing flags that only work in cgroup v1") + SkipIfRootless("many of these handlers are not enabled while rootless in CI") + session := podmanTest.Podman([]string{"run", "-dt", ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + ctrID := session.OutputToString() + + commonArgs := []string{ + "update", + "--cpus", "5", + "--cpuset-cpus", "0", + "--cpu-shares", "123", + "--cpuset-mems", "0", + "--memory", "1G", + "--memory-swap", "2G", + "--memory-reservation", "2G", + "--memory-swappiness", "50", ctrID} + + session = podmanTest.Podman(commonArgs) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + // checking cpu quota from --cpus + session = podmanTest.Podman([]string{"exec", "-it", ctrID, "cat", "/sys/fs/cgroup/cpu/cpu.cfs_quota_us"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).Should(ContainSubstring("500000")) + + // checking cpuset-cpus + session = podmanTest.Podman([]string{"exec", "-it", ctrID, "cat", "/sys/fs/cgroup/cpuset/cpuset.cpus"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).Should(Equal("0")) + + // checking cpuset-mems + session = podmanTest.Podman([]string{"exec", "-it", ctrID, "cat", "/sys/fs/cgroup/cpuset/cpuset.mems"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).Should(Equal("0")) + + // checking memory limit + session = podmanTest.Podman([]string{"exec", "-it", ctrID, "cat", "/sys/fs/cgroup/memory/memory.limit_in_bytes"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).Should(ContainSubstring("1073741824")) + + // checking memory-swap + session = podmanTest.Podman([]string{"exec", "-it", ctrID, "cat", "/sys/fs/cgroup/memory/memory.memsw.limit_in_bytes"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).Should(ContainSubstring("2147483648")) + + // checking cpu-shares + session = podmanTest.Podman([]string{"exec", "-it", ctrID, "cat", "/sys/fs/cgroup/cpu/cpu.shares"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).Should(ContainSubstring("123")) + + }) + + It("podman update container all options v2", func() { + SkipIfCgroupV1("testing flags that only work in cgroup v2") + SkipIfRootless("many of these handlers are not enabled while rootless in CI") + session := podmanTest.Podman([]string{"run", "-dt", ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + ctrID := session.OutputToString() + + commonArgs := []string{ + "update", + "--cpus", "5", + "--cpuset-cpus", "0", + "--cpu-shares", "123", + "--cpuset-mems", "0", + "--memory", "1G", + "--memory-swap", "2G", + "--memory-reservation", "2G", + "--blkio-weight", "123", + "--device-read-bps", "/dev/zero:10mb", + "--device-write-bps", "/dev/zero:10mb", + "--device-read-iops", "/dev/zero:1000", + "--device-write-iops", "/dev/zero:1000", + ctrID} + + session = podmanTest.Podman(commonArgs) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + ctrID = session.OutputToString() + + // checking cpu quota and period + session = podmanTest.Podman([]string{"exec", "-it", ctrID, "cat", "/sys/fs/cgroup/cpu.max"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).Should(ContainSubstring("500000")) + + // checking blkio weight + session = podmanTest.Podman([]string{"exec", "-it", ctrID, "cat", "/sys/fs/cgroup/io.bfq.weight"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).Should(ContainSubstring("123")) + + // checking device-read/write-bps/iops + session = podmanTest.Podman([]string{"exec", "-it", ctrID, "cat", "/sys/fs/cgroup/io.max"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).Should(ContainSubstring("rbps=10485760 wbps=10485760 riops=1000 wiops=1000")) + + // checking cpuset-cpus + session = podmanTest.Podman([]string{"exec", "-it", ctrID, "cat", "/sys/fs/cgroup/cpuset.cpus"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).Should(Equal("0")) + + // checking cpuset-mems + session = podmanTest.Podman([]string{"exec", "-it", ctrID, "cat", "/sys/fs/cgroup/cpuset.mems"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).Should(Equal("0")) + + // checking memory limit + session = podmanTest.Podman([]string{"exec", "-it", ctrID, "cat", "/sys/fs/cgroup/memory.max"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).Should(ContainSubstring("1073741824")) + + // checking memory-swap + session = podmanTest.Podman([]string{"exec", "-it", ctrID, "cat", "/sys/fs/cgroup/memory.swap.max"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).Should(ContainSubstring("1073741824")) + + // checking cpu-shares + session = podmanTest.Podman([]string{"exec", "-it", ctrID, "cat", "/sys/fs/cgroup/cpu.weight"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).Should(ContainSubstring("5")) + }) + + It("podman update keep original resources if not overridden", func() { + SkipIfRootless("many of these handlers are not enabled while rootless in CI") + session := podmanTest.Podman([]string{"run", "-dt", "--cpus", "5", ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + session = podmanTest.Podman([]string{ + "update", + "--memory", "1G", + session.OutputToString(), + }) + + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + ctrID := session.OutputToString() + + if v2, _ := cgroupv2.Enabled(); v2 { + session = podmanTest.Podman([]string{"exec", "-it", ctrID, "cat", "/sys/fs/cgroup/cpu.max"}) + } else { + session = podmanTest.Podman([]string{"exec", "-it", ctrID, "cat", "/sys/fs/cgroup/cpu/cpu.cfs_quota_us"}) + } + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).Should(ContainSubstring("500000")) + }) +}) |