Configure HealthCheck with `podman update`

New flags in a `podman update` can change the configuration of HealthCheck when the container is started, without having to restart or recreate the container.

This can help determine why a given container suddenly started failing HealthCheck without interfering with the services it provides. For example, reconfigure HealthCheck to keep logs longer than the usual last X results, store logs to other destinations, etc.

Fixes: https://issues.redhat.com/browse/RHEL-60561

Signed-off-by: Jan Rodák <hony.com@seznam.cz>
This commit is contained in:
Jan Rodák 2024-10-24 14:01:58 +02:00
parent 77e67e7a54
commit a1249425bd
No known key found for this signature in database
GPG Key ID: D458A9B20435C2BF
34 changed files with 958 additions and 198 deletions

View File

@ -168,78 +168,6 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions,
) )
_ = cmd.RegisterFlagCompletionFunc(groupAddFlagName, completion.AutocompleteNone) _ = 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( createFlags.BoolVar(
&cf.HTTPProxy, &cf.HTTPProxy,
"http-proxy", podmanConfig.ContainersConfDefaultsRO.Containers.HTTPProxy, "http-proxy", podmanConfig.ContainersConfDefaultsRO.Containers.HTTPProxy,
@ -311,11 +239,6 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions,
) )
_ = cmd.RegisterFlagCompletionFunc(logOptFlagName, AutocompleteLogOpt) _ = cmd.RegisterFlagCompletionFunc(logOptFlagName, AutocompleteLogOpt)
createFlags.BoolVar(
&cf.NoHealthCheck,
"no-healthcheck", false,
"Disable healthchecks on container",
)
createFlags.BoolVar( createFlags.BoolVar(
&cf.OOMKillDisable, &cf.OOMKillDisable,
"oom-kill-disable", false, "oom-kill-disable", false,
@ -452,46 +375,6 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions,
) )
_ = cmd.RegisterFlagCompletionFunc(secretFlagName, AutocompleteSecrets) _ = 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" stopSignalFlagName := "stop-signal"
createFlags.StringVar( createFlags.StringVar(
&cf.StopSignal, &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 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 // Restart is allowed for created, updated, and infra ctr
if mode == entities.InfraMode || mode == entities.CreateMode || mode == entities.UpdateMode { if mode == entities.InfraMode || mode == entities.CreateMode || mode == entities.UpdateMode {
restartFlagName := "restart" restartFlagName := "restart"

View File

@ -17,7 +17,7 @@ import (
) )
var ( 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{ updateCommand = &cobra.Command{
Use: "update [options] CONTAINER", Use: "update [options] CONTAINER",
@ -61,6 +61,58 @@ func init() {
updateFlags(containerUpdateCommand) 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 { func update(cmd *cobra.Command, args []string) error {
var err error var err error
// use a specgen since this is the easiest way to hold resource info // 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 return err
} }
healthCheckConfig := GetChangedHealthCheckConfiguration(cmd, &updateOpts)
if err != nil {
return err
}
opts := &entities.ContainerUpdateOptions{ opts := &entities.ContainerUpdateOptions{
NameOrID: strings.TrimPrefix(args[0], "/"), NameOrID: strings.TrimPrefix(args[0], "/"),
Specgen: s, Specgen: s,
ChangedHealthCheckConfiguration: &healthCheckConfig,
} }
rep, err := registry.ContainerEngine().ContainerUpdate(context.Background(), opts) rep, err := registry.ContainerEngine().ContainerUpdate(context.Background(), opts)
if err != nil { if err != nil {

View File

@ -1,5 +1,5 @@
####> This option file is used in: ####> This option file is used in:
####> podman create, run ####> podman create, run, update
####> If file is edited, make sure the changes ####> If file is edited, make sure the changes
####> are applicable to all of those. ####> are applicable to all of those.
#### **--health-cmd**=*"command"* | *'["command", "arg1", ...]'* #### **--health-cmd**=*"command"* | *'["command", "arg1", ...]'*

View File

@ -1,5 +1,5 @@
####> This option file is used in: ####> This option file is used in:
####> podman create, run ####> podman create, run, update
####> If file is edited, make sure the changes ####> If file is edited, make sure the changes
####> are applicable to all of those. ####> are applicable to all of those.
#### **--health-interval**=*interval* #### **--health-interval**=*interval*

View File

@ -1,5 +1,5 @@
####> This option file is used in: ####> This option file is used in:
####> podman create, run ####> podman create, run, update
####> If file is edited, make sure the changes ####> If file is edited, make sure the changes
####> are applicable to all of those. ####> are applicable to all of those.
#### **--health-log-destination**=*directory_path* #### **--health-log-destination**=*directory_path*

View File

@ -1,5 +1,5 @@
####> This option file is used in: ####> This option file is used in:
####> podman create, run ####> podman create, run, update
####> If file is edited, make sure the changes ####> If file is edited, make sure the changes
####> are applicable to all of those. ####> are applicable to all of those.
#### **--health-max-log-count**=*number of stored logs* #### **--health-max-log-count**=*number of stored logs*

View File

@ -1,5 +1,5 @@
####> This option file is used in: ####> This option file is used in:
####> podman create, run ####> podman create, run, update
####> If file is edited, make sure the changes ####> If file is edited, make sure the changes
####> are applicable to all of those. ####> are applicable to all of those.
#### **--health-max-log-size**=*size of stored logs* #### **--health-max-log-size**=*size of stored logs*

View File

@ -1,5 +1,5 @@
####> This option file is used in: ####> This option file is used in:
####> podman create, run ####> podman create, run, update
####> If file is edited, make sure the changes ####> If file is edited, make sure the changes
####> are applicable to all of those. ####> are applicable to all of those.
#### **--health-on-failure**=*action* #### **--health-on-failure**=*action*

View File

@ -1,5 +1,5 @@
####> This option file is used in: ####> This option file is used in:
####> podman create, run ####> podman create, run, update
####> If file is edited, make sure the changes ####> If file is edited, make sure the changes
####> are applicable to all of those. ####> are applicable to all of those.
#### **--health-retries**=*retries* #### **--health-retries**=*retries*

View File

@ -1,5 +1,5 @@
####> This option file is used in: ####> This option file is used in:
####> podman create, run ####> podman create, run, update
####> If file is edited, make sure the changes ####> If file is edited, make sure the changes
####> are applicable to all of those. ####> are applicable to all of those.
#### **--health-start-period**=*period* #### **--health-start-period**=*period*

View File

@ -1,5 +1,5 @@
####> This option file is used in: ####> This option file is used in:
####> podman create, run ####> podman create, run, update
####> If file is edited, make sure the changes ####> If file is edited, make sure the changes
####> are applicable to all of those. ####> are applicable to all of those.
#### **--health-startup-cmd**=*"command"* | *'["command", "arg1", ...]'* #### **--health-startup-cmd**=*"command"* | *'["command", "arg1", ...]'*

View File

@ -1,5 +1,5 @@
####> This option file is used in: ####> This option file is used in:
####> podman create, run ####> podman create, run, update
####> If file is edited, make sure the changes ####> If file is edited, make sure the changes
####> are applicable to all of those. ####> are applicable to all of those.
#### **--health-startup-interval**=*interval* #### **--health-startup-interval**=*interval*

View File

@ -1,5 +1,5 @@
####> This option file is used in: ####> This option file is used in:
####> podman create, run ####> podman create, run, update
####> If file is edited, make sure the changes ####> If file is edited, make sure the changes
####> are applicable to all of those. ####> are applicable to all of those.
#### **--health-startup-retries**=*retries* #### **--health-startup-retries**=*retries*

View File

@ -1,5 +1,5 @@
####> This option file is used in: ####> This option file is used in:
####> podman create, run ####> podman create, run, update
####> If file is edited, make sure the changes ####> If file is edited, make sure the changes
####> are applicable to all of those. ####> are applicable to all of those.
#### **--health-startup-success**=*retries* #### **--health-startup-success**=*retries*

View File

@ -1,5 +1,5 @@
####> This option file is used in: ####> This option file is used in:
####> podman create, run ####> podman create, run, update
####> If file is edited, make sure the changes ####> If file is edited, make sure the changes
####> are applicable to all of those. ####> are applicable to all of those.
#### **--health-startup-timeout**=*timeout* #### **--health-startup-timeout**=*timeout*

View File

@ -1,5 +1,5 @@
####> This option file is used in: ####> This option file is used in:
####> podman create, run ####> podman create, run, update
####> If file is edited, make sure the changes ####> If file is edited, make sure the changes
####> are applicable to all of those. ####> are applicable to all of those.
#### **--health-timeout**=*timeout* #### **--health-timeout**=*timeout*

View File

@ -1,5 +1,5 @@
####> This option file is used in: ####> This option file is used in:
####> podman create, run ####> podman create, run, update
####> If file is edited, make sure the changes ####> If file is edited, make sure the changes
####> are applicable to all of those. ####> are applicable to all of those.
#### **--no-healthcheck** #### **--no-healthcheck**

View File

@ -10,8 +10,7 @@ podman\-update - Update the configuration of a given container
## DESCRIPTION ## DESCRIPTION
Updates the configuration of an already existing container, allowing different resource limits to be set. Updates the configuration of an existing container, allowing changes to resource limits and healthchecks.
The currently supported options are a subset of the podman create/run resource limit options.
## OPTIONS ## OPTIONS
@ -43,6 +42,40 @@ The currently supported options are a subset of the podman create/run resource l
@@option device-write-iops @@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
@@option memory-reservation @@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 memory-swappiness
@@option no-healthcheck
@@option pids-limit @@option pids-limit
@@option restart @@option restart

View File

@ -116,10 +116,11 @@ func (c *Container) Start(ctx context.Context, recursive bool) (finalErr error)
} }
// Update updates the given container. // Update updates the given container.
// Either resource limits or restart policy can be updated. // Either resource limits, restart policies, or HealthCheck configuration can be updated.
// Either resources or restartPolicy must not be nil. // Either resources, restartPolicy or changedHealthCheckConfiguration must not be nil.
// If restartRetries is not nil, restartPolicy must be set and must be "on-failure". // 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 { if !c.batched {
c.lock.Lock() c.lock.Lock()
defer c.lock.Unlock() 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) 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) return c.update(resources, restartPolicy, restartRetries)
} }

View File

@ -2733,8 +2733,123 @@ func (c *Container) update(resources *spec.LinuxResources, restartPolicy *string
} }
logrus.Debugf("updated container %s", c.ID()) logrus.Debugf("updated container %s", c.ID())
return nil
c.newContainerEvent(events.Update) }
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 return nil
} }

View File

@ -2,6 +2,8 @@ package define
import ( import (
"fmt" "fmt"
"os"
"path/filepath"
"strings" "strings"
"github.com/containers/image/v5/manifest" "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. // If set to 0, a single success will mark the HC as passed.
Successes int `json:",omitempty"` 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
}

View File

@ -258,18 +258,6 @@ func (c *Container) incrementStartupHCSuccessCounter(ctx context.Context) {
} }
if recreateTimer { 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. // This kills the process the healthcheck is running.
// Which happens to be us. // Which happens to be us.
// So this has to be last - after this, systemd serves us a // 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 // is the case here as we should not alter the exit code of another process that just
// happened to call this. // happened to call this.
shutdown.SetExitCode(0) shutdown.SetExitCode(0)
if err := c.removeTransientFiles(ctx, true, oldUnit); err != nil { c.recreateHealthCheckTimer(ctx, false, true)
logrus.Errorf("Error removing container %s healthcheck: %v", c.ID(), err) }
return }
}
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
} }
} }

View File

@ -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
}

View File

@ -6,8 +6,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"net" "net"
"os"
"path/filepath"
"strings" "strings"
"syscall" "syscall"
"time" "time"
@ -1521,25 +1519,11 @@ func WithHealthCheckLogDestination(destination string) CtrCreateOption {
if ctr.valid { if ctr.valid {
return define.ErrCtrFinalized return define.ErrCtrFinalized
} }
switch destination { dest, err := define.GetValidHealthCheckDestination(destination)
case define.HealthCheckEventsLoggerDestination, define.DefaultHealthCheckLocalDestination: if err != nil {
ctr.config.HealthLogDestination = destination return err
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
} }
ctr.config.HealthLogDestination = dest
return nil return nil
} }
} }

View File

@ -786,7 +786,7 @@ func UpdateContainer(w http.ResponseWriter, r *http.Request) {
restartRetries = &localRetries 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)) utils.Error(w, http.StatusInternalServerError, fmt.Errorf("updating container: %w", err))
return return
} }

View File

@ -20,7 +20,6 @@ import (
"github.com/containers/podman/v5/pkg/domain/infra/abi" "github.com/containers/podman/v5/pkg/domain/infra/abi"
"github.com/containers/podman/v5/pkg/util" "github.com/containers/podman/v5/pkg/util"
"github.com/gorilla/schema" "github.com/gorilla/schema"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -443,12 +442,12 @@ func UpdateContainer(w http.ResponseWriter, r *http.Request) {
return return
} }
options := &handlers.UpdateEntities{Resources: &specs.LinuxResources{}} options := &handlers.UpdateEntities{}
if err := json.NewDecoder(r.Body).Decode(&options.Resources); err != nil { if err := json.NewDecoder(r.Body).Decode(&options); err != nil {
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("decode(): %w", err)) utils.Error(w, http.StatusInternalServerError, fmt.Errorf("decode(): %w", err))
return return
} }
err = ctr.Update(options.Resources, restartPolicy, restartRetries) err = ctr.Update(&options.LinuxResources, restartPolicy, restartRetries, &options.UpdateHealthCheckConfig)
if err != nil { if err != nil {
utils.InternalServerError(w, err) utils.InternalServerError(w, err)
return return

View File

@ -54,4 +54,6 @@ type networkUpdateRequestLibpod entities.NetworkUpdateOptions
// Container update // Container update
// swagger:model // swagger:model
type containerUpdateRequest container.UpdateConfig type containerUpdateRequest struct {
container.UpdateConfig
}

View File

@ -1,6 +1,7 @@
package handlers package handlers
import ( import (
"github.com/containers/podman/v5/libpod/define"
"github.com/containers/podman/v5/pkg/domain/entities" "github.com/containers/podman/v5/pkg/domain/entities"
docker "github.com/docker/docker/api/types" docker "github.com/docker/docker/api/types"
dockerBackend "github.com/docker/docker/api/types/backend" 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 // UpdateEntities used to wrap the oci resource spec in a swagger model
// swagger:model // swagger:model
type UpdateEntities struct { type UpdateEntities struct {
Resources *specs.LinuxResources specs.LinuxResources
define.UpdateHealthCheckConfig
} }
type Info struct { type Info struct {

View File

@ -1778,8 +1778,8 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error {
// --- // ---
// tags: // tags:
// - containers // - containers
// summary: Update an existing containers cgroup configuration // summary: Updates the configuration of an existing container, allowing changes to resource limits and healthchecks
// description: Update an existing containers cgroup configuration. // description: Updates the configuration of an existing container, allowing changes to resource limits and healthchecks.
// parameters: // parameters:
// - in: path // - in: path
// name: name // name: name

View File

@ -7,6 +7,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/containers/podman/v5/pkg/api/handlers"
"github.com/containers/podman/v5/pkg/bindings" "github.com/containers/podman/v5/pkg/bindings"
"github.com/containers/podman/v5/pkg/domain/entities/types" "github.com/containers/podman/v5/pkg/domain/entities/types"
jsoniter "github.com/json-iterator/go" 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))) params.Set("restartRetries", strconv.Itoa(int(*options.Specgen.RestartRetries)))
} }
} }
updateEntities := &handlers.UpdateEntities{
resources, err := jsoniter.MarshalToString(options.Specgen.ResourceLimits) LinuxResources: *options.Specgen.ResourceLimits,
UpdateHealthCheckConfig: *options.ChangedHealthCheckConfiguration,
}
requestData, err := jsoniter.MarshalToString(updateEntities)
if err != nil { if err != nil {
return "", err 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) response, err := conn.DoRequest(ctx, stringReader, http.MethodPost, "/containers/%s/update", params, nil, options.NameOrID)
if err != nil { if err != nil {
return "", err return "", err

View File

@ -36,6 +36,7 @@ type ContainerStatsReport struct {
} }
type ContainerUpdateOptions struct { type ContainerUpdateOptions struct {
NameOrID string NameOrID string
Specgen *specgen.SpecGenerator Specgen *specgen.SpecGenerator
ChangedHealthCheckConfiguration *define.UpdateHealthCheckConfig
} }

View File

@ -1806,13 +1806,14 @@ func (ic *ContainerEngine) ContainerUpdate(ctx context.Context, updateOptions *e
if len(containers) != 1 { if len(containers) != 1 {
return "", fmt.Errorf("container not found") return "", fmt.Errorf("container not found")
} }
container := containers[0].Container
var restartPolicy *string var restartPolicy *string
if updateOptions.Specgen.RestartPolicy != "" { if updateOptions.Specgen.RestartPolicy != "" {
restartPolicy = &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 "", err
} }
return containers[0].ID(), nil return containers[0].ID(), nil

View File

@ -354,7 +354,7 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions
if c.NoHealthCheck { if c.NoHealthCheck {
return errors.New("cannot specify both --no-healthcheck and --health-cmd") 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 { if err != nil {
return err return err
} }
@ -383,7 +383,7 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions
// The hardcoded "1s" will be discarded, as the startup // The hardcoded "1s" will be discarded, as the startup
// healthcheck does not have a period. So just hardcode // healthcheck does not have a period. So just hardcode
// something that parses correctly. // 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 { if err != nil {
return err return err
} }
@ -948,7 +948,7 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions
return nil 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{} cmdArr := []string{}
isArr := true isArr := true
err := json.Unmarshal([]byte(inCmd), &cmdArr) // array unmarshalling 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") return nil, errors.New("healthcheck-start-period must be 0 seconds or greater")
} }
hc.StartPeriod = startPeriodDuration hc.StartPeriod = startPeriodDuration
return &hc, nil return &hc, nil
} }

View File

@ -161,4 +161,152 @@ device-write-iops = /dev/zero:4000 | - | -
run_podman rm -f -t0 testctr 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" == "<nil>" "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 # vim: filetype=sh