diff --git a/cmd/podman/common/create.go b/cmd/podman/common/create.go index e054d1bdb4..63a9b4bcdd 100644 --- a/cmd/podman/common/create.go +++ b/cmd/podman/common/create.go @@ -440,14 +440,6 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, ) _ = cmd.RegisterFlagCompletionFunc(umaskFlagName, completion.AutocompleteNone) - ulimitFlagName := "ulimit" - createFlags.StringSliceVar( - &cf.Ulimit, - ulimitFlagName, cf.Ulimit, - "Ulimit options", - ) - _ = cmd.RegisterFlagCompletionFunc(ulimitFlagName, completion.AutocompleteNone) - userFlagName := "user" createFlags.StringVarP( &cf.User, @@ -559,6 +551,14 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, ) _ = cmd.RegisterFlagCompletionFunc(unsetenvFlagName, completion.AutocompleteNone) + ulimitFlagName := "ulimit" + createFlags.StringSliceVar( + &cf.Ulimit, + ulimitFlagName, cf.Ulimit, + "Ulimit options", + ) + _ = cmd.RegisterFlagCompletionFunc(ulimitFlagName, completion.AutocompleteNone) + healthCmdFlagName := "health-cmd" createFlags.StringVar( &cf.HealthCmd, diff --git a/cmd/podman/containers/update.go b/cmd/podman/containers/update.go index 6f6ed362c4..6ac1f9dab8 100644 --- a/cmd/podman/containers/update.go +++ b/cmd/podman/containers/update.go @@ -179,6 +179,18 @@ func update(cmd *cobra.Command, args []string) error { opts.UnsetEnv = env } + if cmd.Flags().Changed("ulimit") { + ulimits, err := cmd.Flags().GetStringSlice("ulimit") + if err != nil { + return err + } + rlimits, err := specgenutil.GenRlimits(ulimits) + if err != nil { + return err + } + opts.Rlimits = rlimits + } + rep, err := registry.ContainerEngine().ContainerUpdate(context.Background(), opts) if err != nil { return err diff --git a/docs/source/markdown/options/ulimit.md b/docs/source/markdown/options/ulimit.md index a387b2f02a..82507d480f 100644 --- a/docs/source/markdown/options/ulimit.md +++ b/docs/source/markdown/options/ulimit.md @@ -1,5 +1,5 @@ ####> This option file is used in: -####> podman create, run +####> podman create, run, update ####> If file is edited, make sure the changes ####> are applicable to all of those. #### **--ulimit**=*option* diff --git a/docs/source/markdown/podman-update.1.md.in b/docs/source/markdown/podman-update.1.md.in index 4bfced600f..152c71f9ad 100644 --- a/docs/source/markdown/podman-update.1.md.in +++ b/docs/source/markdown/podman-update.1.md.in @@ -92,6 +92,8 @@ Changing this setting resets the timer, depending on the state of the container. @@option restart +@@option ulimit + @@option unsetenv.update @@ -102,6 +104,11 @@ Update a container with a new cpu quota and period: podman update --cpus=0.5 ctrID ``` +Update a container's ulimit: +``` +podman update --ulimit nofile=1024:1024 myCtr +``` + Update a container with multiple options at ones: ``` podman update --cpus 5 --cpuset-cpus 0 --cpu-shares 123 --cpuset-mems 0 \\ diff --git a/libpod/container_internal.go b/libpod/container_internal.go index 2968d2d48c..4a2de39a65 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -2810,6 +2810,7 @@ func (c *Container) update(updateOptions *entities.ContainerUpdateOptions) error } oldRestart := c.config.RestartPolicy oldRetries := c.config.RestartRetries + oldRlimits := c.config.Spec.Process.Rlimits if updateOptions.RestartPolicy != nil { if err := define.ValidateRestartPolicy(*updateOptions.RestartPolicy); err != nil { @@ -2857,16 +2858,21 @@ func (c *Container) update(updateOptions *entities.ContainerUpdateOptions) error c.config.Spec.Process.Env = envLib.Slice(envMap) } + if updateOptions.Rlimits != nil { + c.config.Spec.Process.Rlimits = updateOptions.Rlimits + } + if err := c.runtime.state.SafeRewriteContainerConfig(c, "", "", c.config); err != nil { // Assume DB write failed, revert to old resources block c.config.Spec.Linux.Resources = oldResources c.config.RestartPolicy = oldRestart c.config.RestartRetries = oldRetries + c.config.Spec.Process.Rlimits = oldRlimits return err } if c.ensureState(define.ContainerStateCreated, define.ContainerStateRunning, define.ContainerStatePaused) && - (updateOptions.Resources != nil || updateOptions.Env != nil || updateOptions.UnsetEnv != nil) { + (updateOptions.Resources != nil || updateOptions.Env != nil || updateOptions.UnsetEnv != nil || updateOptions.Rlimits != nil) { // So `podman inspect` on running containers sources its OCI spec from disk. // To keep inspect accurate we need to update the on-disk OCI spec. onDiskSpec, err := c.specFromState() @@ -2882,6 +2888,9 @@ func (c *Container) update(updateOptions *entities.ContainerUpdateOptions) error if len(updateOptions.Env) != 0 || len(updateOptions.UnsetEnv) != 0 { onDiskSpec.Process.Env = c.config.Spec.Process.Env } + if updateOptions.Rlimits != nil { + onDiskSpec.Process.Rlimits = updateOptions.Rlimits + } if err := c.saveSpec(onDiskSpec); err != nil { logrus.Errorf("Unable to update container %s OCI spec - `podman inspect` may not be accurate until container is restarted: %v", c.ID(), err) } diff --git a/pkg/api/handlers/compat/containers.go b/pkg/api/handlers/compat/containers.go index 596248e43b..660a1b37bc 100644 --- a/pkg/api/handlers/compat/containers.go +++ b/pkg/api/handlers/compat/containers.go @@ -822,11 +822,25 @@ func UpdateContainer(w http.ResponseWriter, r *http.Request) { restartRetries = &localRetries } + // Rlimits + var rlimits []spec.POSIXRlimit + if len(options.Ulimits) > 0 { + for _, ulimit := range options.Ulimits { + rlimit := spec.POSIXRlimit{ + Type: ulimit.Name, + Hard: uint64(ulimit.Hard), + Soft: uint64(ulimit.Soft), + } + rlimits = append(rlimits, rlimit) + } + } + updateOptions := &entities.ContainerUpdateOptions{ Resources: resources, ChangedHealthCheckConfiguration: &define.UpdateHealthCheckConfig{}, RestartPolicy: restartPolicy, RestartRetries: restartRetries, + Rlimits: rlimits, } if err := ctr.Update(updateOptions); err != nil { diff --git a/pkg/api/handlers/libpod/containers.go b/pkg/api/handlers/libpod/containers.go index a7ed9283b3..250cafde93 100644 --- a/pkg/api/handlers/libpod/containers.go +++ b/pkg/api/handlers/libpod/containers.go @@ -462,6 +462,7 @@ func UpdateContainer(w http.ResponseWriter, r *http.Request) { RestartRetries: restartRetries, Env: options.Env, UnsetEnv: options.UnsetEnv, + Rlimits: options.Rlimits, } err = ctr.Update(updateOptions) diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go index 2d85470854..1302af6ce7 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -81,6 +81,7 @@ type UpdateEntities struct { define.UpdateContainerDevicesLimits Env []string UnsetEnv []string + Rlimits []specs.POSIXRlimit } type Info struct { diff --git a/pkg/bindings/containers/update.go b/pkg/bindings/containers/update.go index 574d602b34..fd9b6388c9 100644 --- a/pkg/bindings/containers/update.go +++ b/pkg/bindings/containers/update.go @@ -40,6 +40,9 @@ func Update(ctx context.Context, options *types.ContainerUpdateOptions) (string, if options.DevicesLimits != nil { updateEntities.UpdateContainerDevicesLimits = *options.DevicesLimits } + if options.Rlimits != nil { + updateEntities.Rlimits = options.Rlimits + } requestData, err := jsoniter.MarshalToString(updateEntities) if err != nil { diff --git a/pkg/domain/entities/types/containers.go b/pkg/domain/entities/types/containers.go index 2a85a70eac..d87ada57f3 100644 --- a/pkg/domain/entities/types/containers.go +++ b/pkg/domain/entities/types/containers.go @@ -53,6 +53,7 @@ type ContainerUpdateOptions struct { // - RestartRetries to change restart retries // - Env to change the environment variables. // - UntsetEnv to unset the environment variables. + // - Rlimits to change POSIX resource limits. Specgen *specgen.SpecGenerator Resources *specs.LinuxResources DevicesLimits *define.UpdateContainerDevicesLimits @@ -61,6 +62,7 @@ type ContainerUpdateOptions struct { RestartRetries *uint Env []string UnsetEnv []string + Rlimits []specs.POSIXRlimit } func (u *ContainerUpdateOptions) ProcessSpecgen() { @@ -87,4 +89,8 @@ func (u *ContainerUpdateOptions) ProcessSpecgen() { if u.RestartRetries == nil { u.RestartRetries = u.Specgen.RestartRetries } + + if u.Rlimits == nil { + u.Rlimits = u.Specgen.Rlimits + } } diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index e7111dc5ba..fd0c2cb601 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -1811,15 +1811,15 @@ func (ic *ContainerEngine) ContainerUpdate(ctx context.Context, updateOptions *e if len(containers) != 1 { return "", fmt.Errorf("container not found") } - container := containers[0].Container + ctr := containers[0] updateOptions.Resources, err = specgenutil.UpdateMajorAndMinorNumbers(updateOptions.Resources, updateOptions.DevicesLimits) if err != nil { return "", err } - if err = container.Update(updateOptions); err != nil { + if err = ctr.Container.Update(updateOptions); err != nil { return "", err } - return containers[0].ID(), nil + return ctr.ID(), nil } diff --git a/test/e2e/update_test.go b/test/e2e/update_test.go index e442b7b36d..160132acd3 100644 --- a/test/e2e/update_test.go +++ b/test/e2e/update_test.go @@ -306,4 +306,48 @@ var _ = Describe("Podman update", func() { Expect(env).ToNot(ContainSubstring("FOO")) Expect(env).To(ContainSubstring("PATH=")) }) + + It("podman update sets ulimits", func() { + session := podmanTest.PodmanExitCleanly("run", "-dt", ALPINE) + ctrID := session.OutputToString() + + // Get initial ulimit value + initialUlimit := podmanTest.PodmanExitCleanly("exec", ctrID, "sh", "-c", "ulimit -n") + Expect(initialUlimit.OutputToString()).ToNot(BeEmpty()) + + // Update with a single ulimit + podmanTest.PodmanExitCleanly("update", "--ulimit", "nofile=1024:1024", ctrID) + + // Verify ulimit was updated + ulimit := podmanTest.PodmanExitCleanly("exec", ctrID, "sh", "-c", "ulimit -n") + Expect(ulimit.OutputToString()).To(Equal("1024")) + + // Update with multiple ulimits + podmanTest.PodmanExitCleanly("update", "--ulimit", "nofile=2048:2048", "--ulimit", "nproc=512:512", ctrID) + + // Verify nofile ulimit was updated + ulimit = podmanTest.PodmanExitCleanly("exec", ctrID, "sh", "-c", "ulimit -n") + Expect(ulimit.OutputToString()).To(Equal("2048")) + + // Verify nproc ulimit was updated + ulimit = podmanTest.PodmanExitCleanly("exec", ctrID, "sh", "-c", "ulimit -u") + Expect(ulimit.OutputToString()).To(Equal("512")) + }) + + It("podman update with invalid ulimit fails", func() { + session := podmanTest.PodmanExitCleanly("run", "-dt", ALPINE) + ctrID := session.OutputToString() + + // Try with invalid ulimit syntax (missing =) + update := podmanTest.Podman([]string{"update", "--ulimit", "nofile:1024:1024", ctrID}) + update.WaitWithDefaultTimeout() + Expect(update).Should(Exit(125)) + Expect(update.ErrorToString()).To(ContainSubstring("error")) + + // Try with invalid ulimit value (soft > hard) + update = podmanTest.Podman([]string{"update", "--ulimit", "nofile=2048:1024", ctrID}) + update.WaitWithDefaultTimeout() + Expect(update).Should(Exit(125)) + Expect(update.ErrorToString()).To(ContainSubstring("error")) + }) }) diff --git a/test/system/280-update.bats b/test/system/280-update.bats index 0c87332900..7c509eca25 100644 --- a/test/system/280-update.bats +++ b/test/system/280-update.bats @@ -349,4 +349,54 @@ function nrand() { run_podman rm -t 0 -f "$cid" } + +# bats test_tags=ci:parallel +@test "podman update - set ulimits" { + local ctrname="c-h-$(safename)" + run_podman run -d --name $ctrname $IMAGE sleep 600 + + # Get initial nofile ulimit + run_podman exec $ctrname sh -c "ulimit -n" + initial_nofile="$output" + + # Update with single ulimit + run_podman update --ulimit nofile=1024:1024 $ctrname + + # Verify the nofile ulimit was updated + run_podman exec $ctrname sh -c "ulimit -n" + assert "$output" == "1024" "nofile ulimit updated to 1024" + assert "$output" != "$initial_nofile" "nofile ulimit should have changed" + + # Update with multiple ulimits + run_podman update --ulimit nofile=2048:2048 --ulimit nproc=512:512 $ctrname + + # Verify the nofile ulimit was updated again + run_podman exec $ctrname sh -c "ulimit -n" + assert "$output" == "2048" "nofile ulimit updated to 2048" + + # Verify the nproc ulimit was updated + run_podman exec $ctrname sh -c "ulimit -u" + assert "$output" == "512" "nproc ulimit updated to 512" + + # Test persists across container restart + run_podman restart $ctrname + + # Verify the ulimits persist after restart + run_podman exec $ctrname sh -c "ulimit -n" + assert "$output" == "2048" "nofile ulimit persists after restart" + + run_podman exec $ctrname sh -c "ulimit -u" + assert "$output" == "512" "nproc ulimit persists after restart" + + # Error cases + run_podman 125 update --ulimit nofile:1024:1024 $ctrname + assert "$output" =~ "error" "Invalid ulimit syntax should fail" + + run_podman 125 update --ulimit nofile=2048:1024 $ctrname + assert "$output" =~ "error" "Invalid ulimit values should fail" + + # Clean up + run_podman rm -t 0 -f $ctrname +} + # vim: filetype=sh