diff --git a/cmd/podman/common/create.go b/cmd/podman/common/create.go index 72a47db619..ae18cee3d0 100644 --- a/cmd/podman/common/create.go +++ b/cmd/podman/common/create.go @@ -168,78 +168,6 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, ) _ = cmd.RegisterFlagCompletionFunc(groupAddFlagName, completion.AutocompleteNone) - healthCmdFlagName := "health-cmd" - createFlags.StringVar( - &cf.HealthCmd, - healthCmdFlagName, "", - "set a healthcheck command for the container ('none' disables the existing healthcheck)", - ) - _ = cmd.RegisterFlagCompletionFunc(healthCmdFlagName, completion.AutocompleteNone) - - healthIntervalFlagName := "health-interval" - createFlags.StringVar( - &cf.HealthInterval, - healthIntervalFlagName, define.DefaultHealthCheckInterval, - "set an interval for the healthcheck (a value of disable results in no automatic timer setup)", - ) - _ = cmd.RegisterFlagCompletionFunc(healthIntervalFlagName, completion.AutocompleteNone) - - healthLogDestinationFlagName := "health-log-destination" - createFlags.StringVar( - &cf.HealthLogDestination, - healthLogDestinationFlagName, define.DefaultHealthCheckLocalDestination, - "set the destination of the HealthCheck log. Directory path, local or events_logger (local use container state file)", - ) - _ = cmd.RegisterFlagCompletionFunc(healthLogDestinationFlagName, completion.AutocompleteNone) - - healthMaxLogCountFlagName := "health-max-log-count" - createFlags.UintVar( - &cf.HealthMaxLogCount, - healthMaxLogCountFlagName, define.DefaultHealthMaxLogCount, - "set maximum number of attempts in the HealthCheck log file. ('0' value means an infinite number of attempts in the log file)", - ) - _ = cmd.RegisterFlagCompletionFunc(healthMaxLogCountFlagName, completion.AutocompleteNone) - - healthMaxLogSizeFlagName := "health-max-log-size" - createFlags.UintVar( - &cf.HealthMaxLogSize, - healthMaxLogSizeFlagName, define.DefaultHealthMaxLogSize, - "set maximum length in characters of stored HealthCheck log. ('0' value means an infinite log length)", - ) - _ = cmd.RegisterFlagCompletionFunc(healthMaxLogSizeFlagName, completion.AutocompleteNone) - - healthRetriesFlagName := "health-retries" - createFlags.UintVar( - &cf.HealthRetries, - healthRetriesFlagName, define.DefaultHealthCheckRetries, - "the number of retries allowed before a healthcheck is considered to be unhealthy", - ) - _ = cmd.RegisterFlagCompletionFunc(healthRetriesFlagName, completion.AutocompleteNone) - - healthStartPeriodFlagName := "health-start-period" - createFlags.StringVar( - &cf.HealthStartPeriod, - healthStartPeriodFlagName, define.DefaultHealthCheckStartPeriod, - "the initialization time needed for a container to bootstrap", - ) - _ = cmd.RegisterFlagCompletionFunc(healthStartPeriodFlagName, completion.AutocompleteNone) - - healthTimeoutFlagName := "health-timeout" - createFlags.StringVar( - &cf.HealthTimeout, - healthTimeoutFlagName, define.DefaultHealthCheckTimeout, - "the maximum time allowed to complete the healthcheck before an interval is considered failed", - ) - _ = cmd.RegisterFlagCompletionFunc(healthTimeoutFlagName, completion.AutocompleteNone) - - healthOnFailureFlagName := "health-on-failure" - createFlags.StringVar( - &cf.HealthOnFailure, - healthOnFailureFlagName, "none", - "action to take once the container turns unhealthy", - ) - _ = cmd.RegisterFlagCompletionFunc(healthOnFailureFlagName, AutocompleteHealthOnFailure) - createFlags.BoolVar( &cf.HTTPProxy, "http-proxy", podmanConfig.ContainersConfDefaultsRO.Containers.HTTPProxy, @@ -311,11 +239,6 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, ) _ = cmd.RegisterFlagCompletionFunc(logOptFlagName, AutocompleteLogOpt) - createFlags.BoolVar( - &cf.NoHealthCheck, - "no-healthcheck", false, - "Disable healthchecks on container", - ) createFlags.BoolVar( &cf.OOMKillDisable, "oom-kill-disable", false, @@ -452,46 +375,6 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, ) _ = cmd.RegisterFlagCompletionFunc(secretFlagName, AutocompleteSecrets) - startupHCCmdFlagName := "health-startup-cmd" - createFlags.StringVar( - &cf.StartupHCCmd, - startupHCCmdFlagName, "", - "Set a startup healthcheck command for the container", - ) - _ = cmd.RegisterFlagCompletionFunc(startupHCCmdFlagName, completion.AutocompleteNone) - - startupHCIntervalFlagName := "health-startup-interval" - createFlags.StringVar( - &cf.StartupHCInterval, - startupHCIntervalFlagName, define.DefaultHealthCheckInterval, - "Set an interval for the startup healthcheck", - ) - _ = cmd.RegisterFlagCompletionFunc(startupHCIntervalFlagName, completion.AutocompleteNone) - - startupHCRetriesFlagName := "health-startup-retries" - createFlags.UintVar( - &cf.StartupHCRetries, - startupHCRetriesFlagName, 0, - "Set the maximum number of retries before the startup healthcheck will restart the container", - ) - _ = cmd.RegisterFlagCompletionFunc(startupHCRetriesFlagName, completion.AutocompleteNone) - - startupHCSuccessesFlagName := "health-startup-success" - createFlags.UintVar( - &cf.StartupHCSuccesses, - startupHCSuccessesFlagName, 0, - "Set the number of consecutive successes before the startup healthcheck is marked as successful and the normal healthcheck begins (0 indicates any success will start the regular healthcheck)", - ) - _ = cmd.RegisterFlagCompletionFunc(startupHCSuccessesFlagName, completion.AutocompleteNone) - - startupHCTimeoutFlagName := "health-startup-timeout" - createFlags.StringVar( - &cf.StartupHCTimeout, - startupHCTimeoutFlagName, define.DefaultHealthCheckTimeout, - "Set the maximum amount of time that the startup healthcheck may take before it is considered failed", - ) - _ = cmd.RegisterFlagCompletionFunc(startupHCTimeoutFlagName, completion.AutocompleteNone) - stopSignalFlagName := "stop-signal" createFlags.StringVar( &cf.StopSignal, @@ -665,6 +548,140 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, `If a container with the same name exists, replace it`, ) } + if mode == entities.CreateMode || mode == entities.UpdateMode { + createFlags.BoolVar( + &cf.NoHealthCheck, + "no-healthcheck", false, + "Disable healthchecks on container", + ) + + healthCmdFlagName := "health-cmd" + createFlags.StringVar( + &cf.HealthCmd, + healthCmdFlagName, "", + "set a healthcheck command for the container ('none' disables the existing healthcheck)", + ) + _ = cmd.RegisterFlagCompletionFunc(healthCmdFlagName, completion.AutocompleteNone) + + info := "" + if mode == entities.UpdateMode { + info = "Changing this setting resets timer." + } + healthIntervalFlagName := "health-interval" + createFlags.StringVar( + &cf.HealthInterval, + healthIntervalFlagName, define.DefaultHealthCheckInterval, + "set an interval for the healthcheck. (a value of disable results in no automatic timer setup) "+info, + ) + _ = cmd.RegisterFlagCompletionFunc(healthIntervalFlagName, completion.AutocompleteNone) + + warning := "" + if mode == entities.UpdateMode { + warning = "Warning: Changing this setting may cause the loss of previous logs!" + } + healthLogDestinationFlagName := "health-log-destination" + createFlags.StringVar( + &cf.HealthLogDestination, + healthLogDestinationFlagName, define.DefaultHealthCheckLocalDestination, + "set the destination of the HealthCheck log. Directory path, local or events_logger (local use container state file) "+warning, + ) + _ = cmd.RegisterFlagCompletionFunc(healthLogDestinationFlagName, completion.AutocompleteNone) + + healthMaxLogCountFlagName := "health-max-log-count" + createFlags.UintVar( + &cf.HealthMaxLogCount, + healthMaxLogCountFlagName, define.DefaultHealthMaxLogCount, + "set maximum number of attempts in the HealthCheck log file. ('0' value means an infinite number of attempts in the log file)", + ) + _ = cmd.RegisterFlagCompletionFunc(healthMaxLogCountFlagName, completion.AutocompleteNone) + + healthMaxLogSizeFlagName := "health-max-log-size" + createFlags.UintVar( + &cf.HealthMaxLogSize, + healthMaxLogSizeFlagName, define.DefaultHealthMaxLogSize, + "set maximum length in characters of stored HealthCheck log. ('0' value means an infinite log length)", + ) + _ = cmd.RegisterFlagCompletionFunc(healthMaxLogSizeFlagName, completion.AutocompleteNone) + + healthRetriesFlagName := "health-retries" + createFlags.UintVar( + &cf.HealthRetries, + healthRetriesFlagName, define.DefaultHealthCheckRetries, + "the number of retries allowed before a healthcheck is considered to be unhealthy", + ) + _ = cmd.RegisterFlagCompletionFunc(healthRetriesFlagName, completion.AutocompleteNone) + + healthStartPeriodFlagName := "health-start-period" + createFlags.StringVar( + &cf.HealthStartPeriod, + healthStartPeriodFlagName, define.DefaultHealthCheckStartPeriod, + "the initialization time needed for a container to bootstrap", + ) + _ = cmd.RegisterFlagCompletionFunc(healthStartPeriodFlagName, completion.AutocompleteNone) + + healthTimeoutFlagName := "health-timeout" + createFlags.StringVar( + &cf.HealthTimeout, + healthTimeoutFlagName, define.DefaultHealthCheckTimeout, + "the maximum time allowed to complete the healthcheck before an interval is considered failed", + ) + _ = cmd.RegisterFlagCompletionFunc(healthTimeoutFlagName, completion.AutocompleteNone) + + healthOnFailureFlagName := "health-on-failure" + createFlags.StringVar( + &cf.HealthOnFailure, + healthOnFailureFlagName, "none", + "action to take once the container turns unhealthy", + ) + _ = cmd.RegisterFlagCompletionFunc(healthOnFailureFlagName, AutocompleteHealthOnFailure) + + // Startup HealthCheck + + startupHCCmdFlagName := "health-startup-cmd" + createFlags.StringVar( + &cf.StartupHCCmd, + startupHCCmdFlagName, "", + "Set a startup healthcheck command for the container", + ) + _ = cmd.RegisterFlagCompletionFunc(startupHCCmdFlagName, completion.AutocompleteNone) + + info = "" + if mode == entities.UpdateMode { + info = "Changing this setting resets the timer, depending on the state of the container." + } + startupHCIntervalFlagName := "health-startup-interval" + createFlags.StringVar( + &cf.StartupHCInterval, + startupHCIntervalFlagName, define.DefaultHealthCheckInterval, + "Set an interval for the startup healthcheck. "+info, + ) + _ = cmd.RegisterFlagCompletionFunc(startupHCIntervalFlagName, completion.AutocompleteNone) + + startupHCRetriesFlagName := "health-startup-retries" + createFlags.UintVar( + &cf.StartupHCRetries, + startupHCRetriesFlagName, 0, + "Set the maximum number of retries before the startup healthcheck will restart the container", + ) + _ = cmd.RegisterFlagCompletionFunc(startupHCRetriesFlagName, completion.AutocompleteNone) + + startupHCSuccessesFlagName := "health-startup-success" + createFlags.UintVar( + &cf.StartupHCSuccesses, + startupHCSuccessesFlagName, 0, + "Set the number of consecutive successes before the startup healthcheck is marked as successful and the normal healthcheck begins (0 indicates any success will start the regular healthcheck)", + ) + _ = cmd.RegisterFlagCompletionFunc(startupHCSuccessesFlagName, completion.AutocompleteNone) + + startupHCTimeoutFlagName := "health-startup-timeout" + createFlags.StringVar( + &cf.StartupHCTimeout, + startupHCTimeoutFlagName, define.DefaultHealthCheckTimeout, + "Set the maximum amount of time that the startup healthcheck may take before it is considered failed", + ) + _ = cmd.RegisterFlagCompletionFunc(startupHCTimeoutFlagName, completion.AutocompleteNone) + } + // Restart is allowed for created, updated, and infra ctr if mode == entities.InfraMode || mode == entities.CreateMode || mode == entities.UpdateMode { restartFlagName := "restart" diff --git a/cmd/podman/containers/update.go b/cmd/podman/containers/update.go index 9e8e28070a..2929f37a71 100644 --- a/cmd/podman/containers/update.go +++ b/cmd/podman/containers/update.go @@ -17,7 +17,7 @@ import ( ) var ( - updateDescription = `Updates the cgroup configuration of a given container` + updateDescription = `Updates the configuration of an existing container, allowing changes to resource limits and healthchecks` updateCommand = &cobra.Command{ Use: "update [options] CONTAINER", @@ -61,6 +61,58 @@ func init() { updateFlags(containerUpdateCommand) } +func GetChangedHealthCheckConfiguration(cmd *cobra.Command, vals *entities.ContainerCreateOptions) define.UpdateHealthCheckConfig { + updateHealthCheckConfig := define.UpdateHealthCheckConfig{} + + if cmd.Flags().Changed("health-log-destination") { + updateHealthCheckConfig.HealthLogDestination = &vals.HealthLogDestination + } + if cmd.Flags().Changed("health-max-log-size") { + updateHealthCheckConfig.HealthMaxLogSize = &vals.HealthMaxLogSize + } + if cmd.Flags().Changed("health-max-log-count") { + updateHealthCheckConfig.HealthMaxLogCount = &vals.HealthMaxLogCount + } + if cmd.Flags().Changed("health-on-failure") { + updateHealthCheckConfig.HealthOnFailure = &vals.HealthOnFailure + } + if cmd.Flags().Changed("no-healthcheck") { + updateHealthCheckConfig.NoHealthCheck = &vals.NoHealthCheck + } + if cmd.Flags().Changed("health-cmd") { + updateHealthCheckConfig.HealthCmd = &vals.HealthCmd + } + if cmd.Flags().Changed("health-interval") { + updateHealthCheckConfig.HealthInterval = &vals.HealthInterval + } + if cmd.Flags().Changed("health-retries") { + updateHealthCheckConfig.HealthRetries = &vals.HealthRetries + } + if cmd.Flags().Changed("health-timeout") { + updateHealthCheckConfig.HealthTimeout = &vals.HealthTimeout + } + if cmd.Flags().Changed("health-start-period") { + updateHealthCheckConfig.HealthStartPeriod = &vals.HealthStartPeriod + } + if cmd.Flags().Changed("health-startup-cmd") { + updateHealthCheckConfig.HealthStartupCmd = &vals.StartupHCCmd + } + if cmd.Flags().Changed("health-startup-interval") { + updateHealthCheckConfig.HealthStartupInterval = &vals.StartupHCInterval + } + if cmd.Flags().Changed("health-startup-retries") { + updateHealthCheckConfig.HealthStartupRetries = &vals.StartupHCRetries + } + if cmd.Flags().Changed("health-startup-timeout") { + updateHealthCheckConfig.HealthStartupTimeout = &vals.StartupHCTimeout + } + if cmd.Flags().Changed("health-startup-success") { + updateHealthCheckConfig.HealthStartupSuccess = &vals.StartupHCSuccesses + } + + return updateHealthCheckConfig +} + func update(cmd *cobra.Command, args []string) error { var err error // use a specgen since this is the easiest way to hold resource info @@ -89,9 +141,15 @@ func update(cmd *cobra.Command, args []string) error { return err } + healthCheckConfig := GetChangedHealthCheckConfiguration(cmd, &updateOpts) + if err != nil { + return err + } + opts := &entities.ContainerUpdateOptions{ - NameOrID: strings.TrimPrefix(args[0], "/"), - Specgen: s, + NameOrID: strings.TrimPrefix(args[0], "/"), + Specgen: s, + ChangedHealthCheckConfiguration: &healthCheckConfig, } rep, err := registry.ContainerEngine().ContainerUpdate(context.Background(), opts) if err != nil { diff --git a/docs/source/markdown/options/health-cmd.md b/docs/source/markdown/options/health-cmd.md index a135a2c435..4e09430849 100644 --- a/docs/source/markdown/options/health-cmd.md +++ b/docs/source/markdown/options/health-cmd.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. #### **--health-cmd**=*"command"* | *'["command", "arg1", ...]'* diff --git a/docs/source/markdown/options/health-interval.md b/docs/source/markdown/options/health-interval.md index 9aa86dcd76..0b25517cfa 100644 --- a/docs/source/markdown/options/health-interval.md +++ b/docs/source/markdown/options/health-interval.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. #### **--health-interval**=*interval* diff --git a/docs/source/markdown/options/health-log-destination.md b/docs/source/markdown/options/health-log-destination.md index 16b99ecc4c..91e91e269c 100644 --- a/docs/source/markdown/options/health-log-destination.md +++ b/docs/source/markdown/options/health-log-destination.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. #### **--health-log-destination**=*directory_path* diff --git a/docs/source/markdown/options/health-max-log-count.md b/docs/source/markdown/options/health-max-log-count.md index 96a7d60861..137d470830 100644 --- a/docs/source/markdown/options/health-max-log-count.md +++ b/docs/source/markdown/options/health-max-log-count.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. #### **--health-max-log-count**=*number of stored logs* diff --git a/docs/source/markdown/options/health-max-log-size.md b/docs/source/markdown/options/health-max-log-size.md index 96cc399e4a..1c3169c7f4 100644 --- a/docs/source/markdown/options/health-max-log-size.md +++ b/docs/source/markdown/options/health-max-log-size.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. #### **--health-max-log-size**=*size of stored logs* diff --git a/docs/source/markdown/options/health-on-failure.md b/docs/source/markdown/options/health-on-failure.md index 4075556a2d..6c539e7332 100644 --- a/docs/source/markdown/options/health-on-failure.md +++ b/docs/source/markdown/options/health-on-failure.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. #### **--health-on-failure**=*action* diff --git a/docs/source/markdown/options/health-retries.md b/docs/source/markdown/options/health-retries.md index 224bd0d552..13d68afb95 100644 --- a/docs/source/markdown/options/health-retries.md +++ b/docs/source/markdown/options/health-retries.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. #### **--health-retries**=*retries* diff --git a/docs/source/markdown/options/health-start-period.md b/docs/source/markdown/options/health-start-period.md index 5b1fde4bd5..4391338e46 100644 --- a/docs/source/markdown/options/health-start-period.md +++ b/docs/source/markdown/options/health-start-period.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. #### **--health-start-period**=*period* diff --git a/docs/source/markdown/options/health-startup-cmd.md b/docs/source/markdown/options/health-startup-cmd.md index b3792a584f..67b2db32ad 100644 --- a/docs/source/markdown/options/health-startup-cmd.md +++ b/docs/source/markdown/options/health-startup-cmd.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. #### **--health-startup-cmd**=*"command"* | *'["command", "arg1", ...]'* diff --git a/docs/source/markdown/options/health-startup-interval.md b/docs/source/markdown/options/health-startup-interval.md index dbba969a55..052d703a78 100644 --- a/docs/source/markdown/options/health-startup-interval.md +++ b/docs/source/markdown/options/health-startup-interval.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. #### **--health-startup-interval**=*interval* diff --git a/docs/source/markdown/options/health-startup-retries.md b/docs/source/markdown/options/health-startup-retries.md index db213dcf97..2a0f9fdf90 100644 --- a/docs/source/markdown/options/health-startup-retries.md +++ b/docs/source/markdown/options/health-startup-retries.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. #### **--health-startup-retries**=*retries* diff --git a/docs/source/markdown/options/health-startup-success.md b/docs/source/markdown/options/health-startup-success.md index c8c85e1bfb..e1f911b215 100644 --- a/docs/source/markdown/options/health-startup-success.md +++ b/docs/source/markdown/options/health-startup-success.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. #### **--health-startup-success**=*retries* diff --git a/docs/source/markdown/options/health-startup-timeout.md b/docs/source/markdown/options/health-startup-timeout.md index f6b8c75e07..deffa14025 100644 --- a/docs/source/markdown/options/health-startup-timeout.md +++ b/docs/source/markdown/options/health-startup-timeout.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. #### **--health-startup-timeout**=*timeout* diff --git a/docs/source/markdown/options/health-timeout.md b/docs/source/markdown/options/health-timeout.md index 1324628008..0540a48db3 100644 --- a/docs/source/markdown/options/health-timeout.md +++ b/docs/source/markdown/options/health-timeout.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. #### **--health-timeout**=*timeout* diff --git a/docs/source/markdown/options/no-healthcheck.md b/docs/source/markdown/options/no-healthcheck.md index 14704db8aa..9fab16a563 100644 --- a/docs/source/markdown/options/no-healthcheck.md +++ b/docs/source/markdown/options/no-healthcheck.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. #### **--no-healthcheck** diff --git a/docs/source/markdown/podman-update.1.md.in b/docs/source/markdown/podman-update.1.md.in index 9cce804aa9..de8a7dc252 100644 --- a/docs/source/markdown/podman-update.1.md.in +++ b/docs/source/markdown/podman-update.1.md.in @@ -10,8 +10,7 @@ podman\-update - Update the configuration of a given container ## DESCRIPTION -Updates the configuration of an already existing container, allowing different resource limits to be set. -The currently supported options are a subset of the podman create/run resource limit options. +Updates the configuration of an existing container, allowing changes to resource limits and healthchecks. ## OPTIONS @@ -43,6 +42,40 @@ The currently supported options are a subset of the podman create/run resource l @@option device-write-iops +@@option health-cmd + +@@option health-interval + +Changing this setting resets the timer. + +@@option health-log-destination + +Warning: Changing this setting may cause the loss of previous logs. + +@@option health-max-log-count + +@@option health-max-log-size + +@@option health-on-failure + +@@option health-retries + +@@option health-start-period + +@@option health-startup-cmd + +@@option health-startup-interval + +Changing this setting resets the timer, depending on the state of the container. + +@@option health-startup-retries + +@@option health-startup-success + +@@option health-startup-timeout + +@@option health-timeout + @@option memory @@option memory-reservation @@ -51,6 +84,8 @@ The currently supported options are a subset of the podman create/run resource l @@option memory-swappiness +@@option no-healthcheck + @@option pids-limit @@option restart diff --git a/libpod/container_api.go b/libpod/container_api.go index cae6817ec3..7c95fb9fee 100644 --- a/libpod/container_api.go +++ b/libpod/container_api.go @@ -116,10 +116,11 @@ func (c *Container) Start(ctx context.Context, recursive bool) (finalErr error) } // Update updates the given container. -// Either resource limits or restart policy can be updated. -// Either resources or restartPolicy must not be nil. +// Either resource limits, restart policies, or HealthCheck configuration can be updated. +// Either resources, restartPolicy or changedHealthCheckConfiguration must not be nil. // If restartRetries is not nil, restartPolicy must be set and must be "on-failure". -func (c *Container) Update(resources *spec.LinuxResources, restartPolicy *string, restartRetries *uint) error { +// Nil values of changedHealthCheckConfiguration are not updated. +func (c *Container) Update(resources *spec.LinuxResources, restartPolicy *string, restartRetries *uint, changedHealthCheckConfiguration *define.UpdateHealthCheckConfig) error { if !c.batched { c.lock.Lock() defer c.lock.Unlock() @@ -133,6 +134,41 @@ func (c *Container) Update(resources *spec.LinuxResources, restartPolicy *string return fmt.Errorf("container %s is being removed, cannot update: %w", c.ID(), define.ErrCtrStateInvalid) } + healthCheckConfig, changedHealthCheck, err := GetNewHealthCheckConfig(&HealthCheckConfig{Schema2HealthConfig: c.HealthCheckConfig()}, *changedHealthCheckConfiguration) + if err != nil { + return err + } + if changedHealthCheck { + if err := c.updateHealthCheck( + healthCheckConfig, + &HealthCheckConfig{Schema2HealthConfig: c.config.HealthCheckConfig}, + ); err != nil { + return err + } + } + + startupHealthCheckConfig, changedStartupHealthCheck, err := GetNewHealthCheckConfig(&StartupHealthCheckConfig{StartupHealthCheck: c.Config().StartupHealthCheckConfig}, *changedHealthCheckConfiguration) + if err != nil { + return err + } + if changedStartupHealthCheck { + if err := c.updateHealthCheck( + startupHealthCheckConfig, + &StartupHealthCheckConfig{StartupHealthCheck: c.config.StartupHealthCheckConfig}, + ); err != nil { + return err + } + } + + globalHealthCheckOptions, err := changedHealthCheckConfiguration.GetNewGlobalHealthCheck() + if err != nil { + return err + } + if err := c.updateGlobalHealthCheckConfiguration(globalHealthCheckOptions); err != nil { + return err + } + + defer c.newContainerEvent(events.Update) return c.update(resources, restartPolicy, restartRetries) } diff --git a/libpod/container_internal.go b/libpod/container_internal.go index c7efd18e4b..97e6b47ba8 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -2733,8 +2733,123 @@ func (c *Container) update(resources *spec.LinuxResources, restartPolicy *string } logrus.Debugf("updated container %s", c.ID()) - - c.newContainerEvent(events.Update) - + return nil +} + +func (c *Container) resetHealthCheckTimers(noHealthCheck bool, changedTimer bool, wasEnabledHealthCheck bool, isStartup bool) error { + if !c.ensureState(define.ContainerStateCreated, define.ContainerStateRunning, define.ContainerStatePaused) { + return nil + } + if noHealthCheck { + if err := c.removeTransientFiles(context.Background(), + c.config.StartupHealthCheckConfig != nil && !c.state.StartupHCPassed, + c.state.HCUnitName); err != nil { + return err + } + return nil + } + + if !changedTimer { + return nil + } + + if !isStartup { + if c.state.StartupHCPassed || c.config.StartupHealthCheckConfig == nil { + c.recreateHealthCheckTimer(context.Background(), false, false) + } + return nil + } + + if !c.state.StartupHCPassed { + c.state.StartupHCPassed = !wasEnabledHealthCheck + c.state.StartupHCSuccessCount = 0 + c.state.StartupHCFailureCount = 0 + if err := c.save(); err != nil { + return err + } + if wasEnabledHealthCheck { + c.recreateHealthCheckTimer(context.Background(), true, true) + } + return nil + } + return nil +} + +func (c *Container) updateHealthCheck(newHealthCheckConfig IHealthCheckConfig, currentHealthCheckConfig IHealthCheckConfig) error { + oldHealthCheckConfig := currentHealthCheckConfig + if !oldHealthCheckConfig.IsNil() { + if err := JSONDeepCopy(currentHealthCheckConfig, oldHealthCheckConfig); err != nil { + return err + } + } + + newHealthCheckConfig.SetTo(c.config) + + if err := c.runtime.state.SafeRewriteContainerConfig(c, "", "", c.config); err != nil { + // Assume DB write failed, revert to old resources block + oldHealthCheckConfig.SetTo(c.config) + return err + } + + oldInterval := time.Duration(0) + if !oldHealthCheckConfig.IsNil() { + oldInterval = oldHealthCheckConfig.GetInterval() + } + + changedTimer := false + if !newHealthCheckConfig.IsNil() { + changedTimer = newHealthCheckConfig.IsTimeChanged(oldInterval) + } + + noHealthCheck := c.config.HealthCheckConfig != nil && slices.Contains(c.config.HealthCheckConfig.Test, "NONE") + + if err := c.resetHealthCheckTimers(noHealthCheck, changedTimer, !oldHealthCheckConfig.IsNil(), newHealthCheckConfig.IsStartup()); err != nil { + return err + } + + checkType := "HealthCheck" + if newHealthCheckConfig.IsStartup() { + checkType = "Startup HealthCheck" + } + logrus.Debugf("%s configuration updated for container %s", checkType, c.ID()) + return nil +} + +func (c *Container) updateGlobalHealthCheckConfiguration(globalOptions define.GlobalHealthCheckOptions) error { + oldHealthCheckOnFailureAction := c.config.HealthCheckOnFailureAction + oldHealthLogDestination := c.config.HealthLogDestination + oldHealthMaxLogCount := c.config.HealthMaxLogCount + oldHealthMaxLogSize := c.config.HealthMaxLogSize + + if globalOptions.HealthCheckOnFailureAction != nil { + c.config.HealthCheckOnFailureAction = *globalOptions.HealthCheckOnFailureAction + } + + if globalOptions.HealthMaxLogCount != nil { + c.config.HealthMaxLogCount = *globalOptions.HealthMaxLogCount + } + + if globalOptions.HealthMaxLogSize != nil { + c.config.HealthMaxLogSize = *globalOptions.HealthMaxLogSize + } + + if globalOptions.HealthLogDestination != nil { + dest, err := define.GetValidHealthCheckDestination(*globalOptions.HealthLogDestination) + if err != nil { + return err + } + c.config.HealthLogDestination = dest + } + + if err := c.runtime.state.SafeRewriteContainerConfig(c, "", "", c.config); err != nil { + // Assume DB write failed, revert to old resources block + c.config.HealthCheckOnFailureAction = oldHealthCheckOnFailureAction + c.config.HealthLogDestination = oldHealthLogDestination + c.config.HealthMaxLogCount = oldHealthMaxLogCount + c.config.HealthMaxLogSize = oldHealthMaxLogSize + return err + } + + logrus.Debugf("Global HealthCheck configuration updated for container %s", c.ID()) return nil } diff --git a/libpod/define/healthchecks.go b/libpod/define/healthchecks.go index 4fec277555..17e227366e 100644 --- a/libpod/define/healthchecks.go +++ b/libpod/define/healthchecks.go @@ -2,6 +2,8 @@ package define import ( "fmt" + "os" + "path/filepath" "strings" "github.com/containers/image/v5/manifest" @@ -155,3 +157,186 @@ type StartupHealthCheck struct { // If set to 0, a single success will mark the HC as passed. Successes int `json:",omitempty"` } + +type UpdateHealthCheckConfig struct { + // HealthLogDestination set the destination of the HealthCheck log. + // Directory path, local or events_logger (local use container state file) + // Warning: Changing this setting may cause the loss of previous logs! + HealthLogDestination *string `json:"health_log_destination,omitempty"` + // HealthMaxLogSize set maximum length in characters of stored HealthCheck log. + // ('0' value means an infinite log length) + HealthMaxLogSize *uint `json:"health_max_log_size,omitempty"` + // HealthMaxLogCount set maximum number of attempts in the HealthCheck log file. + // ('0' value means an infinite number of attempts in the log file) + HealthMaxLogCount *uint `json:"health_max_log_count,omitempty"` + // HealthOnFailure set the action to take once the container turns unhealthy. + HealthOnFailure *string `json:"health_on_failure,omitempty"` + // Disable healthchecks on container. + NoHealthCheck *bool `json:"no_healthcheck,omitempty"` + // HealthCmd set a healthcheck command for the container. ('none' disables the existing healthcheck) + HealthCmd *string `json:"health_cmd,omitempty"` + // HealthInterval set an interval for the healthcheck. + // (a value of disable results in no automatic timer setup) Changing this setting resets timer. + HealthInterval *string `json:"health_interval,omitempty"` + // HealthRetries set the number of retries allowed before a healthcheck is considered to be unhealthy. + HealthRetries *uint `json:"health_retries,omitempty"` + // HealthTimeout set the maximum time allowed to complete the healthcheck before an interval is considered failed. + HealthTimeout *string `json:"health_timeout,omitempty"` + // HealthStartPeriod set the initialization time needed for a container to bootstrap. + HealthStartPeriod *string `json:"health_start_period,omitempty"` + // HealthStartupCmd set a startup healthcheck command for the container. + HealthStartupCmd *string `json:"health_startup_cmd,omitempty"` + // HealthStartupInterval set an interval for the startup healthcheck. + // Changing this setting resets the timer, depending on the state of the container. + HealthStartupInterval *string `json:"health_startup_interval,omitempty"` + // HealthStartupRetries set the maximum number of retries before the startup healthcheck will restart the container. + HealthStartupRetries *uint `json:"health_startup_retries,omitempty"` + // HealthStartupTimeout set the maximum amount of time that the startup healthcheck may take before it is considered failed. + HealthStartupTimeout *string `json:"health_startup_timeout,omitempty"` + // HealthStartupSuccess set the number of consecutive successes before the startup healthcheck is marked as successful + // and the normal healthcheck begins (0 indicates any success will start the regular healthcheck) + HealthStartupSuccess *uint `json:"health_startup_success,omitempty"` +} + +func (u *UpdateHealthCheckConfig) IsStartupHealthCheckCommandSet(startupHealthCheck *StartupHealthCheck) bool { + containsStartupHealthCheckCmd := u.HealthStartupCmd != nil + containsFlags := (u.HealthStartupInterval != nil || u.HealthStartupRetries != nil || + u.HealthStartupTimeout != nil || u.HealthStartupSuccess != nil) + return startupHealthCheck == nil && !containsStartupHealthCheckCmd && containsFlags +} + +func (u *UpdateHealthCheckConfig) IsHealthCheckCommandSet(healthCheck *manifest.Schema2HealthConfig) bool { + containsStartupHealthCheckCmd := u.HealthCmd != nil + containsFlags := (u.HealthInterval != nil || u.HealthRetries != nil || + u.HealthTimeout != nil || u.HealthStartPeriod != nil) + return healthCheck == nil && !containsStartupHealthCheckCmd && containsFlags +} + +func (u *UpdateHealthCheckConfig) SetNewStartupHealthCheckConfigTo(healthCheckOptions *HealthCheckOptions) bool { + changed := false + + if u.HealthStartupCmd != nil { + healthCheckOptions.Cmd = *u.HealthStartupCmd + changed = true + } + + if u.HealthStartupInterval != nil { + healthCheckOptions.Interval = *u.HealthStartupInterval + changed = true + } + + if u.HealthStartupRetries != nil { + healthCheckOptions.Retries = int(*u.HealthStartupRetries) + changed = true + } + + if u.HealthStartupTimeout != nil { + healthCheckOptions.Timeout = *u.HealthStartupTimeout + changed = true + } + + if u.HealthStartupSuccess != nil { + healthCheckOptions.Successes = int(*u.HealthStartupSuccess) + changed = true + } + + healthCheckOptions.StartPeriod = "1s" + + return changed +} + +func (u *UpdateHealthCheckConfig) SetNewHealthCheckConfigTo(healthCheckOptions *HealthCheckOptions) bool { + changed := false + + if u.HealthCmd != nil { + healthCheckOptions.Cmd = *u.HealthCmd + changed = true + } + + if u.HealthInterval != nil { + healthCheckOptions.Interval = *u.HealthInterval + changed = true + } + + if u.HealthRetries != nil { + healthCheckOptions.Retries = int(*u.HealthRetries) + changed = true + } + + if u.HealthTimeout != nil { + healthCheckOptions.Timeout = *u.HealthTimeout + changed = true + } + + if u.HealthStartPeriod != nil { + healthCheckOptions.StartPeriod = *u.HealthStartPeriod + changed = true + } + + return changed +} + +func GetValidHealthCheckDestination(destination string) (string, error) { + if destination == HealthCheckEventsLoggerDestination || destination == DefaultHealthCheckLocalDestination { + return destination, nil + } + + fileInfo, err := os.Stat(destination) + if err != nil { + return "", fmt.Errorf("HealthCheck Log '%s' destination error: %w", destination, err) + } + mode := fileInfo.Mode() + if !mode.IsDir() { + return "", fmt.Errorf("HealthCheck Log '%s' destination must be directory", destination) + } + + absPath, err := filepath.Abs(destination) + if err != nil { + return "", err + } + return absPath, nil +} + +func (u *UpdateHealthCheckConfig) GetNewGlobalHealthCheck() (GlobalHealthCheckOptions, error) { + globalOptions := GlobalHealthCheckOptions{} + + healthLogDestination := u.HealthLogDestination + if u.HealthLogDestination != nil { + dest, err := GetValidHealthCheckDestination(*u.HealthLogDestination) + if err != nil { + return GlobalHealthCheckOptions{}, err + } + healthLogDestination = &dest + } + globalOptions.HealthLogDestination = healthLogDestination + + globalOptions.HealthMaxLogSize = u.HealthMaxLogSize + + globalOptions.HealthMaxLogCount = u.HealthMaxLogCount + + if u.HealthOnFailure != nil { + val, err := ParseHealthCheckOnFailureAction(*u.HealthOnFailure) + if err != nil { + return globalOptions, err + } + globalOptions.HealthCheckOnFailureAction = &val + } + + return globalOptions, nil +} + +type HealthCheckOptions struct { + Cmd string + Interval string + Retries int + Timeout string + StartPeriod string + Successes int +} + +type GlobalHealthCheckOptions struct { + HealthLogDestination *string + HealthMaxLogCount *uint + HealthMaxLogSize *uint + HealthCheckOnFailureAction *HealthCheckOnFailureAction +} diff --git a/libpod/healthcheck.go b/libpod/healthcheck.go index ada4e004d3..32723d6cf7 100644 --- a/libpod/healthcheck.go +++ b/libpod/healthcheck.go @@ -258,18 +258,6 @@ func (c *Container) incrementStartupHCSuccessCounter(ctx context.Context) { } if recreateTimer { - logrus.Infof("Startup healthcheck for container %s passed, recreating timer", c.ID()) - - oldUnit := c.state.HCUnitName - // Create the new, standard healthcheck timer first. - if err := c.createTimer(c.HealthCheckConfig().Interval.String(), false); err != nil { - logrus.Errorf("Error recreating container %s healthcheck: %v", c.ID(), err) - return - } - if err := c.startTimer(false); err != nil { - logrus.Errorf("Error restarting container %s healthcheck timer: %v", c.ID(), err) - } - // This kills the process the healthcheck is running. // Which happens to be us. // So this has to be last - after this, systemd serves us a @@ -281,10 +269,31 @@ func (c *Container) incrementStartupHCSuccessCounter(ctx context.Context) { // is the case here as we should not alter the exit code of another process that just // happened to call this. shutdown.SetExitCode(0) - if err := c.removeTransientFiles(ctx, true, oldUnit); err != nil { - logrus.Errorf("Error removing container %s healthcheck: %v", c.ID(), err) - return - } + c.recreateHealthCheckTimer(ctx, false, true) + } +} + +func (c *Container) recreateHealthCheckTimer(ctx context.Context, isStartup bool, isStartupRemoved bool) { + logrus.Infof("Startup healthcheck for container %s passed, recreating timer", c.ID()) + + oldUnit := c.state.HCUnitName + // Create the new, standard healthcheck timer first. + interval := c.HealthCheckConfig().Interval.String() + if isStartup { + interval = c.config.StartupHealthCheckConfig.StartInterval.String() + } + + if err := c.createTimer(interval, isStartup); err != nil { + logrus.Errorf("Error recreating container %s (isStartup: %t) healthcheck: %v", c.ID(), isStartup, err) + return + } + if err := c.startTimer(isStartup); err != nil { + logrus.Errorf("Error restarting container %s (isStartup: %t) healthcheck timer: %v", c.ID(), isStartup, err) + } + + if err := c.removeTransientFiles(ctx, isStartupRemoved, oldUnit); err != nil { + logrus.Errorf("Error removing container %s healthcheck: %v", c.ID(), err) + return } } diff --git a/libpod/healthcheck_config.go b/libpod/healthcheck_config.go new file mode 100644 index 0000000000..8b9b25833b --- /dev/null +++ b/libpod/healthcheck_config.go @@ -0,0 +1,165 @@ +//go:build !remote + +package libpod + +import ( + "errors" + "strings" + "time" + + "github.com/containers/image/v5/manifest" + "github.com/containers/podman/v5/libpod/define" + "github.com/containers/podman/v5/pkg/specgenutil" +) + +type HealthCheckConfig struct { + *manifest.Schema2HealthConfig +} + +type StartupHealthCheckConfig struct { + *define.StartupHealthCheck +} + +type IHealthCheckConfig interface { + SetTo(config *ContainerConfig) + IsStartup() bool + IsNil() bool + IsTimeChanged(oldInterval time.Duration) bool + GetInterval() time.Duration + SetCurrentConfigTo(healthCheckOptions *define.HealthCheckOptions) + IsHealthCheckCommandSet(updateHealthCheckConfig define.UpdateHealthCheckConfig) bool + SetNewHealthCheckOptions(updateHealthCheckConfig define.UpdateHealthCheckConfig, healthCheckOptions *define.HealthCheckOptions) bool +} + +func (h *HealthCheckConfig) SetTo(config *ContainerConfig) { + config.HealthCheckConfig = h.Schema2HealthConfig +} + +func (h *StartupHealthCheckConfig) SetTo(config *ContainerConfig) { + config.StartupHealthCheckConfig = h.StartupHealthCheck +} + +func (h *HealthCheckConfig) IsNil() bool { + return h.Schema2HealthConfig == nil +} + +func (h *StartupHealthCheckConfig) IsNil() bool { + return h.StartupHealthCheck == nil +} + +func (h *HealthCheckConfig) IsStartup() bool { + return false +} + +func (h *StartupHealthCheckConfig) IsStartup() bool { + return true +} + +func (h *HealthCheckConfig) IsTimeChanged(oldInterval time.Duration) bool { + return h.Interval != oldInterval +} + +func (h *StartupHealthCheckConfig) IsTimeChanged(oldInterval time.Duration) bool { + return h.Interval != oldInterval +} + +func (h *HealthCheckConfig) GetInterval() time.Duration { + return h.Interval +} + +func (h *StartupHealthCheckConfig) GetInterval() time.Duration { + return h.Interval +} + +func (h *HealthCheckConfig) SetCurrentConfigTo(healthCheckOptions *define.HealthCheckOptions) { + healthCheckOptions.Cmd = strings.Join(h.Test, " ") + healthCheckOptions.Interval = h.Interval.String() + healthCheckOptions.Retries = h.Retries + healthCheckOptions.Timeout = h.Timeout.String() + healthCheckOptions.StartPeriod = h.StartPeriod.String() +} + +func (h *StartupHealthCheckConfig) SetCurrentConfigTo(healthCheckOptions *define.HealthCheckOptions) { + healthCheckOptions.Cmd = strings.Join(h.Test, " ") + healthCheckOptions.Interval = h.Interval.String() + healthCheckOptions.Retries = h.Retries + healthCheckOptions.Timeout = h.Timeout.String() + healthCheckOptions.Successes = h.Successes +} + +func (h *HealthCheckConfig) IsHealthCheckCommandSet(updateHealthCheckConfig define.UpdateHealthCheckConfig) bool { + return updateHealthCheckConfig.IsHealthCheckCommandSet(h.Schema2HealthConfig) +} + +func (h *StartupHealthCheckConfig) IsHealthCheckCommandSet(updateHealthCheckConfig define.UpdateHealthCheckConfig) bool { + return updateHealthCheckConfig.IsStartupHealthCheckCommandSet(h.StartupHealthCheck) +} + +func (h *HealthCheckConfig) SetNewHealthCheckOptions(updateHealthCheckConfig define.UpdateHealthCheckConfig, healthCheckOptions *define.HealthCheckOptions) bool { + return updateHealthCheckConfig.SetNewHealthCheckConfigTo(healthCheckOptions) +} + +func (h *StartupHealthCheckConfig) SetNewHealthCheckOptions(updateHealthCheckConfig define.UpdateHealthCheckConfig, healthCheckOptions *define.HealthCheckOptions) bool { + return updateHealthCheckConfig.SetNewStartupHealthCheckConfigTo(healthCheckOptions) +} + +func GetNewHealthCheckConfig(originalHealthCheckConfig IHealthCheckConfig, updateHealthCheckConfig define.UpdateHealthCheckConfig) (IHealthCheckConfig, bool, error) { + if originalHealthCheckConfig.IsHealthCheckCommandSet(updateHealthCheckConfig) { + return nil, false, errors.New("startup healthcheck command is not set") + } + + healthCheckOptions := define.HealthCheckOptions{ + Cmd: "", + Interval: define.DefaultHealthCheckInterval, + Retries: int(define.DefaultHealthCheckRetries), + Timeout: define.DefaultHealthCheckTimeout, + StartPeriod: define.DefaultHealthCheckStartPeriod, + Successes: 0, + } + + if originalHealthCheckConfig.IsStartup() { + healthCheckOptions.Retries = 0 + } + + if !originalHealthCheckConfig.IsNil() { + originalHealthCheckConfig.SetCurrentConfigTo(&healthCheckOptions) + } + + noHealthCheck := false + if updateHealthCheckConfig.NoHealthCheck != nil { + noHealthCheck = *updateHealthCheckConfig.NoHealthCheck + } + + changed := originalHealthCheckConfig.SetNewHealthCheckOptions(updateHealthCheckConfig, &healthCheckOptions) + + if noHealthCheck && changed { + return nil, false, errors.New("cannot specify both --no-healthcheck and other HealthCheck flags") + } + + if noHealthCheck { + if originalHealthCheckConfig.IsStartup() { + return &StartupHealthCheckConfig{StartupHealthCheck: nil}, true, nil + } + return &HealthCheckConfig{Schema2HealthConfig: &manifest.Schema2HealthConfig{Test: []string{"NONE"}}}, true, nil + } + + newHealthCheckConfig, err := specgenutil.MakeHealthCheckFromCli( + healthCheckOptions.Cmd, + healthCheckOptions.Interval, + uint(healthCheckOptions.Retries), + healthCheckOptions.Timeout, + healthCheckOptions.StartPeriod, + true, + ) + if err != nil { + return nil, false, err + } + + if originalHealthCheckConfig.IsStartup() { + newStartupHealthCheckConfig := new(define.StartupHealthCheck) + newStartupHealthCheckConfig.Schema2HealthConfig = *newHealthCheckConfig + newStartupHealthCheckConfig.Successes = healthCheckOptions.Successes + return &StartupHealthCheckConfig{StartupHealthCheck: newStartupHealthCheckConfig}, changed, nil + } + return &HealthCheckConfig{Schema2HealthConfig: newHealthCheckConfig}, changed, nil +} diff --git a/libpod/options.go b/libpod/options.go index 2569f9fd11..9a5c7cd1b8 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -6,8 +6,6 @@ import ( "errors" "fmt" "net" - "os" - "path/filepath" "strings" "syscall" "time" @@ -1521,25 +1519,11 @@ func WithHealthCheckLogDestination(destination string) CtrCreateOption { if ctr.valid { return define.ErrCtrFinalized } - switch destination { - case define.HealthCheckEventsLoggerDestination, define.DefaultHealthCheckLocalDestination: - ctr.config.HealthLogDestination = destination - default: - fileInfo, err := os.Stat(destination) - if err != nil { - return fmt.Errorf("HealthCheck Log '%s' destination error: %w", destination, err) - } - mode := fileInfo.Mode() - if !mode.IsDir() { - return fmt.Errorf("HealthCheck Log '%s' destination must be directory", destination) - } - - absPath, err := filepath.Abs(destination) - if err != nil { - return err - } - ctr.config.HealthLogDestination = absPath + dest, err := define.GetValidHealthCheckDestination(destination) + if err != nil { + return err } + ctr.config.HealthLogDestination = dest return nil } } diff --git a/pkg/api/handlers/compat/containers.go b/pkg/api/handlers/compat/containers.go index 706d2d97ba..487c989e78 100644 --- a/pkg/api/handlers/compat/containers.go +++ b/pkg/api/handlers/compat/containers.go @@ -786,7 +786,7 @@ func UpdateContainer(w http.ResponseWriter, r *http.Request) { restartRetries = &localRetries } - if err := ctr.Update(resources, restartPolicy, restartRetries); err != nil { + if err := ctr.Update(resources, restartPolicy, restartRetries, &define.UpdateHealthCheckConfig{}); err != nil { utils.Error(w, http.StatusInternalServerError, fmt.Errorf("updating container: %w", err)) return } diff --git a/pkg/api/handlers/libpod/containers.go b/pkg/api/handlers/libpod/containers.go index 726c30b2e7..d9ec050dc9 100644 --- a/pkg/api/handlers/libpod/containers.go +++ b/pkg/api/handlers/libpod/containers.go @@ -20,7 +20,6 @@ import ( "github.com/containers/podman/v5/pkg/domain/infra/abi" "github.com/containers/podman/v5/pkg/util" "github.com/gorilla/schema" - "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" ) @@ -443,12 +442,12 @@ func UpdateContainer(w http.ResponseWriter, r *http.Request) { return } - options := &handlers.UpdateEntities{Resources: &specs.LinuxResources{}} - if err := json.NewDecoder(r.Body).Decode(&options.Resources); err != nil { + options := &handlers.UpdateEntities{} + if err := json.NewDecoder(r.Body).Decode(&options); err != nil { utils.Error(w, http.StatusInternalServerError, fmt.Errorf("decode(): %w", err)) return } - err = ctr.Update(options.Resources, restartPolicy, restartRetries) + err = ctr.Update(&options.LinuxResources, restartPolicy, restartRetries, &options.UpdateHealthCheckConfig) if err != nil { utils.InternalServerError(w, err) return diff --git a/pkg/api/handlers/swagger/models.go b/pkg/api/handlers/swagger/models.go index 56fd5d8b56..4438b94b8c 100644 --- a/pkg/api/handlers/swagger/models.go +++ b/pkg/api/handlers/swagger/models.go @@ -54,4 +54,6 @@ type networkUpdateRequestLibpod entities.NetworkUpdateOptions // Container update // swagger:model -type containerUpdateRequest container.UpdateConfig +type containerUpdateRequest struct { + container.UpdateConfig +} diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go index 5a13530b90..d461344a35 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -1,6 +1,7 @@ package handlers import ( + "github.com/containers/podman/v5/libpod/define" "github.com/containers/podman/v5/pkg/domain/entities" docker "github.com/docker/docker/api/types" dockerBackend "github.com/docker/docker/api/types/backend" @@ -73,7 +74,8 @@ type LibpodContainersRmReport struct { // UpdateEntities used to wrap the oci resource spec in a swagger model // swagger:model type UpdateEntities struct { - Resources *specs.LinuxResources + specs.LinuxResources + define.UpdateHealthCheckConfig } type Info struct { diff --git a/pkg/api/server/register_containers.go b/pkg/api/server/register_containers.go index cc1cc5e74d..3adfb2a90f 100644 --- a/pkg/api/server/register_containers.go +++ b/pkg/api/server/register_containers.go @@ -1778,8 +1778,8 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // --- // tags: // - containers - // summary: Update an existing containers cgroup configuration - // description: Update an existing containers cgroup configuration. + // summary: Updates the configuration of an existing container, allowing changes to resource limits and healthchecks + // description: Updates the configuration of an existing container, allowing changes to resource limits and healthchecks. // parameters: // - in: path // name: name diff --git a/pkg/bindings/containers/update.go b/pkg/bindings/containers/update.go index 37cf74426e..8091158029 100644 --- a/pkg/bindings/containers/update.go +++ b/pkg/bindings/containers/update.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" + "github.com/containers/podman/v5/pkg/api/handlers" "github.com/containers/podman/v5/pkg/bindings" "github.com/containers/podman/v5/pkg/domain/entities/types" jsoniter "github.com/json-iterator/go" @@ -25,12 +26,15 @@ func Update(ctx context.Context, options *types.ContainerUpdateOptions) (string, params.Set("restartRetries", strconv.Itoa(int(*options.Specgen.RestartRetries))) } } - - resources, err := jsoniter.MarshalToString(options.Specgen.ResourceLimits) + updateEntities := &handlers.UpdateEntities{ + LinuxResources: *options.Specgen.ResourceLimits, + UpdateHealthCheckConfig: *options.ChangedHealthCheckConfiguration, + } + requestData, err := jsoniter.MarshalToString(updateEntities) if err != nil { return "", err } - stringReader := strings.NewReader(resources) + stringReader := strings.NewReader(requestData) response, err := conn.DoRequest(ctx, stringReader, http.MethodPost, "/containers/%s/update", params, nil, options.NameOrID) if err != nil { return "", err diff --git a/pkg/domain/entities/types/containers.go b/pkg/domain/entities/types/containers.go index f9d922e229..0e1f563931 100644 --- a/pkg/domain/entities/types/containers.go +++ b/pkg/domain/entities/types/containers.go @@ -36,6 +36,7 @@ type ContainerStatsReport struct { } type ContainerUpdateOptions struct { - NameOrID string - Specgen *specgen.SpecGenerator + NameOrID string + Specgen *specgen.SpecGenerator + ChangedHealthCheckConfiguration *define.UpdateHealthCheckConfig } diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index 0a0dbe3357..10bc46d21a 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -1806,13 +1806,14 @@ func (ic *ContainerEngine) ContainerUpdate(ctx context.Context, updateOptions *e if len(containers) != 1 { return "", fmt.Errorf("container not found") } + container := containers[0].Container var restartPolicy *string if updateOptions.Specgen.RestartPolicy != "" { restartPolicy = &updateOptions.Specgen.RestartPolicy } - if err = containers[0].Update(updateOptions.Specgen.ResourceLimits, restartPolicy, updateOptions.Specgen.RestartRetries); err != nil { + if err = container.Update(updateOptions.Specgen.ResourceLimits, restartPolicy, updateOptions.Specgen.RestartRetries, updateOptions.ChangedHealthCheckConfiguration); err != nil { return "", err } return containers[0].ID(), nil diff --git a/pkg/specgenutil/specgen.go b/pkg/specgenutil/specgen.go index 0bc9b419a7..1c76acaff9 100644 --- a/pkg/specgenutil/specgen.go +++ b/pkg/specgenutil/specgen.go @@ -354,7 +354,7 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions if c.NoHealthCheck { return errors.New("cannot specify both --no-healthcheck and --health-cmd") } - s.HealthConfig, err = makeHealthCheckFromCli(c.HealthCmd, c.HealthInterval, c.HealthRetries, c.HealthTimeout, c.HealthStartPeriod, false) + s.HealthConfig, err = MakeHealthCheckFromCli(c.HealthCmd, c.HealthInterval, c.HealthRetries, c.HealthTimeout, c.HealthStartPeriod, false) if err != nil { return err } @@ -383,7 +383,7 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions // The hardcoded "1s" will be discarded, as the startup // healthcheck does not have a period. So just hardcode // something that parses correctly. - tmpHcConfig, err := makeHealthCheckFromCli(c.StartupHCCmd, c.StartupHCInterval, c.StartupHCRetries, c.StartupHCTimeout, "1s", true) + tmpHcConfig, err := MakeHealthCheckFromCli(c.StartupHCCmd, c.StartupHCInterval, c.StartupHCRetries, c.StartupHCTimeout, "1s", true) if err != nil { return err } @@ -948,7 +948,7 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions return nil } -func makeHealthCheckFromCli(inCmd, interval string, retries uint, timeout, startPeriod string, isStartup bool) (*manifest.Schema2HealthConfig, error) { +func MakeHealthCheckFromCli(inCmd, interval string, retries uint, timeout, startPeriod string, isStartup bool) (*manifest.Schema2HealthConfig, error) { cmdArr := []string{} isArr := true err := json.Unmarshal([]byte(inCmd), &cmdArr) // array unmarshalling @@ -1017,7 +1017,6 @@ func makeHealthCheckFromCli(inCmd, interval string, retries uint, timeout, start return nil, errors.New("healthcheck-start-period must be 0 seconds or greater") } hc.StartPeriod = startPeriodDuration - return &hc, nil } diff --git a/test/system/280-update.bats b/test/system/280-update.bats index 6013aa17e7..f4c8a9640a 100644 --- a/test/system/280-update.bats +++ b/test/system/280-update.bats @@ -161,4 +161,152 @@ device-write-iops = /dev/zero:4000 | - | - run_podman rm -f -t0 testctr } +# HealthCheck configuration + +function nrand() { + # 1-59 seconds. Don't exceed 59, because podman then shows as "1mXXs" + echo $((1 + RANDOM % 58)) +} + +# bats test_tags=ci:parallel +@test "podman update - test all HealthCheck flags" { + local ctrname="c-h-$(safename)" + local msg="healthmsg-$(random_string)" + local TMP_DIR_HEALTHCHECK="$PODMAN_TMPDIR/healthcheck" + mkdir $TMP_DIR_HEALTHCHECK + + # flag-name | value | inspect format, .Config.Xxx + tests=" + cmd | echo $msg | Healthcheck.Test + interval | $(nrand)s | Healthcheck.Interval + log-destination | $TMP_DIR_HEALTHCHECK | HealthLogDestination + max-log-count | $(nrand) | HealthMaxLogCount + max-log-size | $(nrand) | HealthMaxLogSize + on-failure | restart | HealthcheckOnFailureAction + retries | $(nrand) | Healthcheck.Retries + timeout | $(nrand)s | Healthcheck.Timeout + start-period | $(nrand)s | Healthcheck.StartPeriod + startup-cmd | echo $msg | StartupHealthCheck.Test + startup-interval | $(nrand)s | StartupHealthCheck.Interval + startup-retries | $(nrand) | StartupHealthCheck.Retries + startup-success | $(nrand) | StartupHealthCheck.Successes + startup-timeout | $(nrand)s | StartupHealthCheck.Timeout + " + + run_podman run -d --name $ctrname $IMAGE top + cid="$output" + + # Pass 1: read the table above, gather up the options, format and expected values + local -a opts + local -A formats + local -A checks + while read opt value format ; do + fullopt="--health-$opt=$value" + opts+=("$fullopt") + formats["$fullopt"]="{{.Config.$format}}" + expected=$value + # Special case for commands + if [[ $opt =~ cmd ]]; then + expected="[CMD-SHELL $value]" + fi + checks["$fullopt"]=$expected + done < <(parse_table "$tests") + + # Now do the update in one fell swoop + run_podman update "${opts[@]}" $ctrname + + # ...and check one by one + defer-assertion-failures + for opt in "${opts[@]}"; do + run_podman inspect $ctrname --format "${formats[$opt]}" + assert "$output" == "${checks[$opt]}" "$opt" + done + immediate-assertion-failures + + # Clean up + run_podman rm -f -t0 $cid +} + +# bats test_tags=ci:parallel +@test "podman update - test HealthCheck flags without HealthCheck commands" { + local ctrname="c-h-$(safename)" + + # flag-name=value + tests=" + interval=10s + retries=5 + timeout=10s + start-period=10s + startup-interval=10s + startup-retries=5 + startup-success=10 + startup-timeout=10s + " + + run_podman run -d --name $ctrname $IMAGE top + cid="$output" + + defer-assertion-failures + for opt in $tests; do + run_podman 125 update "--health-$opt" $ctrname + assert "$output" =~ "healthcheck command is not set" "--$opt with no startup" + done + immediate-assertion-failures + + run_podman rm -f -t0 $cid +} + +# bats test_tags=ci:parallel +@test "podman update - --no-healthcheck" { + local msg="healthmsg-$(random_string)" + local ctrname="c-h-$(safename)" + + run_podman run -d --name $ctrname \ + --health-cmd "echo $msg" \ + --health-startup-cmd "echo startup$msg" \ + $IMAGE /home/podman/pause + cid="$output" + + run_podman update $ctrname --no-healthcheck + + run_podman inspect $ctrname --format {{.Config.Healthcheck.Test}} + assert "$output" == "[NONE]" "HealthCheck command is disabled" + + run_podman inspect $ctrname --format {{.Config.StartupHealthCheck}} + assert "$output" == "" "startup HealthCheck command is disabled" + + run_podman rm -t 0 -f $ctrname +} + +# bats test_tags=ci:parallel +@test "podman update - check behavior - change cmd and destination healthcheck" { + local TMP_DIR_HEALTHCHECK="$PODMAN_TMPDIR/healthcheck" + mkdir $TMP_DIR_HEALTHCHECK + local ctrname="c-h-$(safename)" + local msg="healthmsg-$(random_string)" + + run_podman run -d --name $ctrname \ + --health-cmd "echo $msg" \ + $IMAGE /home/podman/pause + cid="$output" + + run_podman healthcheck run $ctrname + is "$output" "" "output from 'podman healthcheck run'" + + # Run podman update in two separate runs to make sure HealthCheck is overwritten correctly. + run_podman update $ctrname --health-cmd "echo healthmsg-new" + + run_podman update $ctrname --health-log-destination $TMP_DIR_HEALTHCHECK + + run_podman healthcheck run $ctrname + is "$output" "" "output from 'podman healthcheck run'" + + healthcheck_log_path="${TMP_DIR_HEALTHCHECK}/${cid}-healthcheck.log" + # The healthcheck is triggered by the podman when the container is started, but its execution depends on systemd. + # And since `run_podman healthcheck run` is also run manually, it will result in two runs. + count=$(grep -co "healthmsg-new" $healthcheck_log_path) + assert "$count" -ge 1 "Number of matching health log messages" + + run_podman rm -t 0 -f $ctrname +} # vim: filetype=sh