diff --git a/cmd/podman/common/create.go b/cmd/podman/common/create.go index 6de40987eeef..8d3bcd4c2be1 100644 --- a/cmd/podman/common/create.go +++ b/cmd/podman/common/create.go @@ -184,6 +184,14 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, ) _ = cmd.RegisterFlagCompletionFunc(healthIntervalFlagName, completion.AutocompleteNone) + healthMaxLogCountFlagName := "health-max-log-count" + createFlags.UintVar( + &cf.HealthMaxLogCount, + healthMaxLogCountFlagName, define.DefaultHealthMaxLogCount, + "set the maximum number of attempts we keep in the healthcheck history file. ('0' value means no limit for stored logs)", + ) + _ = cmd.RegisterFlagCompletionFunc(healthMaxLogCountFlagName, completion.AutocompleteNone) + healthRetriesFlagName := "health-retries" createFlags.UintVar( &cf.HealthRetries, diff --git a/docs/source/markdown/options/health-max-log-count.md b/docs/source/markdown/options/health-max-log-count.md new file mode 100644 index 000000000000..056a0244c6a0 --- /dev/null +++ b/docs/source/markdown/options/health-max-log-count.md @@ -0,0 +1,7 @@ +####> This option file is used in: +####> podman create, run +####> If file is edited, make sure the changes +####> are applicable to all of those. +#### **--health-max-log-count**=*number of stored logs* + +Set the maximum number of attempts we keep in the healthcheck history file. ('0' value means no limit for stored logs) diff --git a/docs/source/markdown/podman-create.1.md.in b/docs/source/markdown/podman-create.1.md.in index d0a939e8733d..8f97a5625721 100644 --- a/docs/source/markdown/podman-create.1.md.in +++ b/docs/source/markdown/podman-create.1.md.in @@ -169,6 +169,8 @@ See [**Environment**](#environment) note below for precedence and examples. @@option health-interval +@@option health-max-log-count + @@option health-on-failure @@option health-retries diff --git a/docs/source/markdown/podman-run.1.md.in b/docs/source/markdown/podman-run.1.md.in index da546a3354e0..fffd5768c43f 100644 --- a/docs/source/markdown/podman-run.1.md.in +++ b/docs/source/markdown/podman-run.1.md.in @@ -203,6 +203,8 @@ See [**Environment**](#environment) note below for precedence and examples. @@option health-interval +@@option health-max-log-count + @@option health-on-failure @@option health-retries diff --git a/docs/source/markdown/podman-systemd.unit.5.md b/docs/source/markdown/podman-systemd.unit.5.md index 846fdb97ba4d..fddb3f288edf 100644 --- a/docs/source/markdown/podman-systemd.unit.5.md +++ b/docs/source/markdown/podman-systemd.unit.5.md @@ -279,6 +279,7 @@ Valid options for `[Container]` are listed below: | GroupAdd=keep-groups | --group-add=keep-groups | | HealthCmd=/usr/bin/command | --health-cmd=/usr/bin/command | | HealthInterval=2m | --health-interval=2m | +| HealthMaxLogCount=5 | --health-max-log-count=5 | | HealthOnFailure=kill | --health-on-failure=kill | | HealthRetries=5 | --health-retries=5 | | HealthStartPeriod=1m | --health-start-period=period=1m | @@ -515,6 +516,11 @@ Equivalent to the Podman `--health-cmd` option. Set an interval for the healthchecks. An interval of disable results in no automatic timer setup. Equivalent to the Podman `--health-interval` option. +### `HealthMaxLogCount=` + +Set the maximum number of attempts we keep in the healthcheck history file. ('0' value means no limit for stored logs) +Equivalent to the Podman `--Health-max-log-count` option. + ### `HealthOnFailure=` Action to take once the container transitions to an unhealthy state. diff --git a/libpod/container_config.go b/libpod/container_config.go index 8c4e0176c5b3..229d519a93e0 100644 --- a/libpod/container_config.go +++ b/libpod/container_config.go @@ -413,6 +413,9 @@ type ContainerMiscConfig struct { HealthCheckConfig *manifest.Schema2HealthConfig `json:"healthcheck"` // HealthCheckOnFailureAction defines an action to take once the container turns unhealthy. HealthCheckOnFailureAction define.HealthCheckOnFailureAction `json:"healthcheck_on_failure_action"` + // HealthMaxLogCount is the maximum number of attempts we keep + // in the healthcheck history file ("0" value means no limit) + HealthMaxLogCount uint `json:"healthMaxLogCount"` // StartupHealthCheckConfig is the configuration of the startup // healthcheck for the container. This will run before the regular HC // runs, and when it passes the regular HC will be activated. diff --git a/libpod/container_inspect.go b/libpod/container_inspect.go index aa561a5cdc9d..3887884340d2 100644 --- a/libpod/container_inspect.go +++ b/libpod/container_inspect.go @@ -412,6 +412,8 @@ func (c *Container) generateInspectContainerConfig(spec *spec.Spec) *define.Insp ctrConfig.HealthcheckOnFailureAction = c.config.HealthCheckOnFailureAction.String() + ctrConfig.HealthMaxLogCount = c.config.HealthMaxLogCount + ctrConfig.CreateCommand = c.config.CreateCommand ctrConfig.Timezone = c.config.Timezone diff --git a/libpod/define/container_inspect.go b/libpod/define/container_inspect.go index 89b70e59e659..3dab01b0331c 100644 --- a/libpod/define/container_inspect.go +++ b/libpod/define/container_inspect.go @@ -61,6 +61,9 @@ type InspectContainerConfig struct { Healthcheck *manifest.Schema2HealthConfig `json:"Healthcheck,omitempty"` // HealthcheckOnFailureAction defines an action to take once the container turns unhealthy. HealthcheckOnFailureAction string `json:"HealthcheckOnFailureAction,omitempty"` + // HealthMaxLogCount is the maximum number of attempts we keep + // in the healthcheck history file ("0" value means no limit) + HealthMaxLogCount uint `json:"HealthcheckMaxLogCount,omitempty"` // CreateCommand is the full command plus arguments of the process the // container has been created with. CreateCommand []string `json:"CreateCommand,omitempty"` diff --git a/libpod/define/healthchecks.go b/libpod/define/healthchecks.go index 15ea79fc20c0..a43a23714e95 100644 --- a/libpod/define/healthchecks.go +++ b/libpod/define/healthchecks.go @@ -56,6 +56,8 @@ const ( DefaultHealthCheckStartPeriod = "0s" // DefaultHealthCheckTimeout default value DefaultHealthCheckTimeout = "30s" + // DefaultHealthMaxLogCount default value + DefaultHealthMaxLogCount uint = 5 ) // HealthConfig.Test options diff --git a/libpod/healthcheck.go b/libpod/healthcheck.go index 95f20f2fd1bd..c724613c5958 100644 --- a/libpod/healthcheck.go +++ b/libpod/healthcheck.go @@ -20,9 +20,6 @@ import ( ) const ( - // MaxHealthCheckNumberLogs is the maximum number of attempts we keep - // in the healthcheck history file - MaxHealthCheckNumberLogs int = 5 // MaxHealthCheckLogLength in characters MaxHealthCheckLogLength = 500 ) @@ -398,7 +395,7 @@ func (c *Container) updateHealthCheckLog(hcl define.HealthCheckLog, inStartPerio } } healthCheck.Log = append(healthCheck.Log, hcl) - if len(healthCheck.Log) > MaxHealthCheckNumberLogs { + if c.config.HealthMaxLogCount != 0 && len(healthCheck.Log) > int(c.config.HealthMaxLogCount) { healthCheck.Log = healthCheck.Log[1:] } newResults, err := json.Marshal(healthCheck) diff --git a/libpod/options.go b/libpod/options.go index 9f30f2f3250a..d9887487b425 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1500,6 +1500,17 @@ func WithHealthCheck(healthCheck *manifest.Schema2HealthConfig) CtrCreateOption } } +// WithHealthCheckMaxLogCount adds the healthCheckMaxLogCount to the container config +func WithHealthCheckMaxLogCount(maxLogCount uint) CtrCreateOption { + return func(ctr *Container) error { + if ctr.valid { + return define.ErrCtrFinalized + } + ctr.config.HealthMaxLogCount = maxLogCount + return nil + } +} + // WithHealthCheckOnFailureAction adds an on-failure action to health-check config func WithHealthCheckOnFailureAction(action define.HealthCheckOnFailureAction) CtrCreateOption { return func(ctr *Container) error { diff --git a/pkg/domain/entities/pods.go b/pkg/domain/entities/pods.go index 63b88e3355ad..f1e4e28f051b 100644 --- a/pkg/domain/entities/pods.go +++ b/pkg/domain/entities/pods.go @@ -171,6 +171,7 @@ type ContainerCreateOptions struct { HealthCmd string HealthInterval string HealthRetries uint + HealthMaxLogCount uint HealthStartPeriod string HealthTimeout string HealthOnFailure string diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go index 8d4029114bc1..dac974bd74b6 100644 --- a/pkg/specgen/generate/container_create.go +++ b/pkg/specgen/generate/container_create.go @@ -642,6 +642,10 @@ func createContainerOptions(rt *libpod.Runtime, s *specgen.SpecGenerator, pod *l options = append(options, libpod.WithHealthCheckOnFailureAction(s.ContainerHealthCheckConfig.HealthCheckOnFailureAction)) } + if healthCheckSet { + options = append(options, libpod.WithHealthCheckMaxLogCount(s.ContainerHealthCheckConfig.HealthMaxLogCount)) + } + if s.SdNotifyMode == define.SdNotifyModeHealthy && !healthCheckSet { return nil, fmt.Errorf("%w: sdnotify policy %q requires a healthcheck to be set", define.ErrInvalidArg, s.SdNotifyMode) } diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index ab4aeb7aec99..1f798ee891b5 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -599,6 +599,9 @@ type ContainerHealthCheckConfig struct { // Requires that HealthConfig be set. // Optional. StartupHealthConfig *define.StartupHealthCheck `json:"startupHealthConfig,omitempty"` + // HealthMaxLogCount is the maximum number of attempts we keep + // in the healthcheck history file ("0" value means no limit) + HealthMaxLogCount uint `json:"healthMaxLogCount,omitempty"` } // SpecGenerator creates an OCI spec and Libpod configuration options to create diff --git a/pkg/specgenutil/specgen.go b/pkg/specgenutil/specgen.go index 6cb1f154d5a9..958d94855603 100644 --- a/pkg/specgenutil/specgen.go +++ b/pkg/specgenutil/specgen.go @@ -370,6 +370,8 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions } s.HealthCheckOnFailureAction = onFailureAction + s.HealthMaxLogCount = c.HealthMaxLogCount + if c.StartupHCCmd != "" { if c.NoHealthCheck { return errors.New("cannot specify both --no-healthcheck and --health-startup-cmd") diff --git a/pkg/systemd/quadlet/quadlet.go b/pkg/systemd/quadlet/quadlet.go index 092256ec3579..fbbf19c9d21b 100644 --- a/pkg/systemd/quadlet/quadlet.go +++ b/pkg/systemd/quadlet/quadlet.go @@ -93,6 +93,7 @@ const ( KeyGroupAdd = "GroupAdd" KeyHealthCmd = "HealthCmd" KeyHealthInterval = "HealthInterval" + KeyHealthMaxLogCount = "HealthMaxLogCount" KeyHealthOnFailure = "HealthOnFailure" KeyHealthRetries = "HealthRetries" KeyHealthStartPeriod = "HealthStartPeriod" @@ -214,6 +215,7 @@ var ( KeyHealthCmd: true, KeyHealthInterval: true, KeyHealthOnFailure: true, + KeyHealthMaxLogCount: true, KeyHealthRetries: true, KeyHealthStartPeriod: true, KeyHealthStartupCmd: true, @@ -2065,6 +2067,7 @@ func handleHealth(unitFile *parser.UnitFile, groupName string, podman *PodmanCmd {KeyHealthCmd, "cmd"}, {KeyHealthInterval, "interval"}, {KeyHealthOnFailure, "on-failure"}, + {KeyHealthMaxLogCount, "max-log-count"}, {KeyHealthRetries, "retries"}, {KeyHealthStartPeriod, "start-period"}, {KeyHealthTimeout, "timeout"}, diff --git a/test/e2e/healthcheck_run_test.go b/test/e2e/healthcheck_run_test.go index b65419569fcf..9567b31dd4aa 100644 --- a/test/e2e/healthcheck_run_test.go +++ b/test/e2e/healthcheck_run_test.go @@ -390,4 +390,56 @@ HEALTHCHECK CMD ls -l / 2>&1`, ALPINE) Expect(ps.OutputToStringArray()).To(HaveLen(2)) Expect(ps.OutputToString()).To(ContainSubstring("hc")) }) + + It("Healthcheck with max default value (5) of last execution log", func() { + countOfExecutions := 10 + ctrName := "hc" + ctrRun := podmanTest.Podman([]string{"run", "-dt", "--name", ctrName, "--health-cmd", "echo hello", ALPINE, "top"}) + ctrRun.WaitWithDefaultTimeout() + Expect(ctrRun).Should(ExitCleanly()) + + for i := 0; i < countOfExecutions; i++ { + hc := podmanTest.Podman([]string{"healthcheck", "run", ctrName}) + hc.WaitWithDefaultTimeout() + Expect(hc).Should(ExitCleanly()) + } + + inspect := podmanTest.InspectContainer(ctrName) + Expect(inspect[0].State.Health.Log).To(HaveLen(5)) + }) + + It("Healthcheck with max infinite value (0) of last execution log", func() { + countOfExecutions := 12 + ctrName := "hc" + ctrRun := podmanTest.Podman([]string{"run", "-dt", "--name", ctrName, "--health-cmd", "echo hello", "--health-max-log-count", "0", ALPINE, "top"}) + ctrRun.WaitWithDefaultTimeout() + Expect(ctrRun).Should(ExitCleanly()) + + for i := 0; i < countOfExecutions; i++ { + hc := podmanTest.Podman([]string{"healthcheck", "run", ctrName}) + hc.WaitWithDefaultTimeout() + Expect(hc).Should(ExitCleanly()) + } + + inspect := podmanTest.InspectContainer(ctrName) + Expect(inspect[0].State.Health.Log).To(HaveLen(countOfExecutions)) + }) + + It("Healthcheck with max 10 last execution log", func() { + countOfExecutions := 10 + ctrName := "hc" + ctrRun := podmanTest.Podman([]string{"run", "-dt", "--name", ctrName, "--health-cmd", "echo hello", "--health-max-log-count", strconv.Itoa(countOfExecutions), ALPINE, "top"}) + ctrRun.WaitWithDefaultTimeout() + Expect(ctrRun).Should(ExitCleanly()) + + for i := 0; i < countOfExecutions; i++ { + hc := podmanTest.Podman([]string{"healthcheck", "run", ctrName}) + hc.WaitWithDefaultTimeout() + Expect(hc).Should(ExitCleanly()) + } + + inspect := podmanTest.InspectContainer(ctrName) + Expect(inspect[0].State.Health.Log).To(HaveLen(countOfExecutions)) + }) + }) diff --git a/test/system/220-healthcheck.bats b/test/system/220-healthcheck.bats index 7e01a5545bca..eac7678fecef 100644 --- a/test/system/220-healthcheck.bats +++ b/test/system/220-healthcheck.bats @@ -268,4 +268,84 @@ Log[-1].Output | \"Uh-oh on stdout!\\\nUh-oh on stderr!\\\n\" done } +@test "podman healthcheck --health-max-log-count default value (5)" { + local repeat_count=10 + local msg="Hello, How are you?" + local ctrname="c-h-$(safename)" + run_podman run -d --name $ctrname \ + --health-cmd "echo $msg" \ + $IMAGE /home/podman/pause + cid="$output" + + run_podman inspect $ctrname --format "{{.Config.HealthMaxLogCount}}" + is "$output" "5" "HealthMaxLogCount is set to 5" + + for _ in $(seq 1 $repeat_count); + do + run_podman healthcheck run $ctrname + is "$output" "" "output from 'podman healthcheck run'" + done + + + run_podman inspect $ctrname --format "{{.State.Health.Log}}" + [ $(echo "$output" | grep -o "$msg" - | wc -l) -eq 5 ] + + run_podman rm -t 0 -f $ctrname +} + +@test "podman healthcheck --health-max-log-count infinite value (0)" { + local repeat_count=10 + local msg="Hello, How are you?" + local ctrname="c-h-$(safename)" + run_podman run -d --name $ctrname \ + --health-cmd "echo $msg" \ + --health-max-log-count 0 \ + $IMAGE /home/podman/pause + cid="$output" + + run_podman inspect $ctrname --format "{{.Config.HealthMaxLogCount}}" + is "$output" "0" "HealthMaxLogCount is set to 0" + + # This is run 11 times to check that the cap is working. + for _ in $(seq 0 $repeat_count); + do + run_podman healthcheck run $ctrname + is "$output" "" "output from 'podman healthcheck run'" + done + + + run_podman inspect $ctrname --format "{{.State.Health.Log}}" + [ $(echo "$output" | grep -o "$msg" - | wc -l) -ge 11 ] + + run_podman rm -t 0 -f $ctrname +} + + +@test "podman healthcheck --health-max-log-count 10" { + local repeat_count=10 + local msg="Hello, How are you?" + local ctrname="c-h-$(safename)" + run_podman run -d --name $ctrname \ + --health-cmd "echo $msg" \ + --health-max-log-count $repeat_count\ + $IMAGE /home/podman/pause + cid="$output" + + run_podman inspect $ctrname --format "{{.Config.HealthMaxLogCount}}" + is "$output" "10" "HealthMaxLogCount is set to 10" + + # This is run 11 times to check that the cap is working. + for _ in $(seq 0 $repeat_count); + do + run_podman healthcheck run $ctrname + is "$output" "" "output from 'podman healthcheck run'" + done + + + run_podman inspect $ctrname --format "{{.State.Health.Log}}" + [ $(echo "$output" | grep -o "$msg" - | wc -l) -eq $repeat_count ] + + run_podman rm -t 0 -f $ctrname +} + # vim: filetype=sh