diff options
-rw-r--r-- | cmd/podman/cliconfig/config.go | 4 | ||||
-rw-r--r-- | cmd/podman/commands.go | 7 | ||||
-rw-r--r-- | cmd/podman/commands_remoteclient.go | 5 | ||||
-rw-r--r-- | cmd/podman/create.go | 25 | ||||
-rw-r--r-- | cmd/podman/healthcheck.go | 25 | ||||
-rw-r--r-- | cmd/podman/healthcheck_run.go | 53 | ||||
-rw-r--r-- | completions/bash/podman | 42 | ||||
-rw-r--r-- | docs/podman-healthcheck-run.1.md | 39 | ||||
-rw-r--r-- | docs/podman-healthcheck.1.md | 22 | ||||
-rw-r--r-- | libpod/container.go | 15 | ||||
-rw-r--r-- | libpod/healthcheck.go | 92 | ||||
-rw-r--r-- | libpod/image/image.go | 29 | ||||
-rw-r--r-- | libpod/options.go | 12 | ||||
-rw-r--r-- | pkg/adapter/runtime.go | 5 | ||||
-rw-r--r-- | pkg/adapter/runtime_remote.go | 5 | ||||
-rw-r--r-- | pkg/spec/createconfig.go | 7 | ||||
-rw-r--r-- | test/e2e/config.go | 1 | ||||
-rw-r--r-- | test/e2e/config_amd64.go | 2 | ||||
-rw-r--r-- | test/e2e/healthcheck_run_test.go | 85 | ||||
-rw-r--r-- | transfer.md | 1 |
20 files changed, 475 insertions, 1 deletions
diff --git a/cmd/podman/cliconfig/config.go b/cmd/podman/cliconfig/config.go index ea99aafc2..7945cb6cb 100644 --- a/cmd/podman/cliconfig/config.go +++ b/cmd/podman/cliconfig/config.go @@ -217,6 +217,10 @@ type PauseValues struct { All bool } +type HealthCheckValues struct { + PodmanCommand +} + type KubePlayValues struct { PodmanCommand Authfile string diff --git a/cmd/podman/commands.go b/cmd/podman/commands.go index c261e54e2..150413970 100644 --- a/cmd/podman/commands.go +++ b/cmd/podman/commands.go @@ -121,3 +121,10 @@ func getSystemSubCommands() []*cobra.Command { _renumberCommand, } } + +// Commands that the local client implements +func getHealtcheckSubCommands() []*cobra.Command { + return []*cobra.Command{ + _healthcheckrunCommand, + } +} diff --git a/cmd/podman/commands_remoteclient.go b/cmd/podman/commands_remoteclient.go index 081043b25..f0f815d78 100644 --- a/cmd/podman/commands_remoteclient.go +++ b/cmd/podman/commands_remoteclient.go @@ -52,3 +52,8 @@ func getTrustSubCommands() []*cobra.Command { func getSystemSubCommands() []*cobra.Command { return []*cobra.Command{} } + +// Commands that the remoteclient implements +func getHealtcheckSubCommands() []*cobra.Command { + return []*cobra.Command{} +} diff --git a/cmd/podman/create.go b/cmd/podman/create.go index 129c886b2..a7b9bbf31 100644 --- a/cmd/podman/create.go +++ b/cmd/podman/create.go @@ -12,6 +12,7 @@ import ( "strings" "syscall" + "github.com/containers/image/manifest" "github.com/containers/libpod/cmd/podman/cliconfig" "github.com/containers/libpod/cmd/podman/libpodruntime" "github.com/containers/libpod/cmd/podman/shared" @@ -117,6 +118,10 @@ func createInit(c *cliconfig.PodmanCommand) error { } func createContainer(c *cliconfig.PodmanCommand, runtime *libpod.Runtime) (*libpod.Container, *cc.CreateConfig, error) { + var ( + hasHealthCheck bool + healthCheck *manifest.Schema2HealthConfig + ) if c.Bool("trace") { span, _ := opentracing.StartSpanFromContext(Ctx, "createContainer") defer span.Finish() @@ -163,12 +168,32 @@ func createContainer(c *cliconfig.PodmanCommand, runtime *libpod.Runtime) (*libp } else { imageName = newImage.ID() } + + // add healthcheck if it exists AND is correct mediatype + _, mediaType, err := newImage.Manifest(ctx) + if err != nil { + return nil, nil, errors.Wrapf(err, "unable to determine mediatype of image %s", newImage.ID()) + } + if mediaType == manifest.DockerV2Schema2MediaType { + healthCheck, err = newImage.GetHealthCheck(ctx) + if err != nil { + return nil, nil, errors.Wrapf(err, "unable to get healthcheck for %s", c.InputArgs[0]) + } + if healthCheck != nil { + hasHealthCheck = true + } + } } createConfig, err := parseCreateOpts(ctx, c, runtime, imageName, data) if err != nil { return nil, nil, err } + // Because parseCreateOpts does derive anything from the image, we add health check + // at this point. The rest is done by WithOptions. + createConfig.HasHealthCheck = hasHealthCheck + createConfig.HealthCheck = healthCheck + ctr, err := createContainerFromCreateConfig(runtime, createConfig, ctx, nil) if err != nil { return nil, nil, err diff --git a/cmd/podman/healthcheck.go b/cmd/podman/healthcheck.go new file mode 100644 index 000000000..e7cc125cc --- /dev/null +++ b/cmd/podman/healthcheck.go @@ -0,0 +1,25 @@ +package main + +import ( + "github.com/containers/libpod/cmd/podman/cliconfig" + "github.com/spf13/cobra" +) + +var healthcheckDescription = "Manage health checks on containers" +var healthcheckCommand = cliconfig.PodmanCommand{ + Command: &cobra.Command{ + Use: "healthcheck", + Short: "Manage Healthcheck", + Long: healthcheckDescription, + }, +} + +// Commands that are universally implemented +var healthcheckCommands []*cobra.Command + +func init() { + healthcheckCommand.AddCommand(healthcheckCommands...) + healthcheckCommand.AddCommand(getHealtcheckSubCommands()...) + healthcheckCommand.SetUsageTemplate(UsageTemplate()) + rootCmd.AddCommand(healthcheckCommand.Command) +} diff --git a/cmd/podman/healthcheck_run.go b/cmd/podman/healthcheck_run.go new file mode 100644 index 000000000..a91f87146 --- /dev/null +++ b/cmd/podman/healthcheck_run.go @@ -0,0 +1,53 @@ +package main + +import ( + "fmt" + "github.com/containers/libpod/cmd/podman/cliconfig" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/adapter" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + healthcheckRunCommand cliconfig.HealthCheckValues + healthcheckRunDescription = "run the health check of a container" + _healthcheckrunCommand = &cobra.Command{ + Use: "run CONTAINER", + Short: "run the health check of a container", + Long: healthcheckRunDescription, + Example: `podman healthcheck run mywebapp`, + RunE: func(cmd *cobra.Command, args []string) error { + healthcheckRunCommand.InputArgs = args + healthcheckRunCommand.GlobalFlags = MainGlobalOpts + return healthCheckCmd(&healthcheckRunCommand) + }, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) < 1 || len(args) > 1 { + return errors.New("must provide the name or ID of one container") + } + return nil + }, + } +) + +func init() { + healthcheckRunCommand.Command = _healthcheckrunCommand + healthcheckRunCommand.SetUsageTemplate(UsageTemplate()) +} + +func healthCheckCmd(c *cliconfig.HealthCheckValues) error { + runtime, err := adapter.GetRuntime(&c.PodmanCommand) + if err != nil { + return errors.Wrap(err, "could not get runtime") + } + status, err := runtime.HealthCheck(c) + if err != nil { + if status == libpod.HealthCheckFailure { + fmt.Println("\nunhealthy") + } + return err + } + fmt.Println("\nhealthy") + return nil +} diff --git a/completions/bash/podman b/completions/bash/podman index 74e3a49d2..0022772c7 100644 --- a/completions/bash/podman +++ b/completions/bash/podman @@ -888,6 +888,26 @@ _podman_container_wait() { _podman_wait } +_podman_healthcheck() { + local boolean_options=" + --help + -h + " + subcommands=" + run + " + __podman_subcommands "$subcommands $aliases" && return + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help" -- "$cur" ) ) + ;; + *) + COMPREPLY=( $( compgen -W "$subcommands" -- "$cur" ) ) + ;; + esac +} + _podman_generate() { local boolean_options=" --help @@ -2338,6 +2358,27 @@ _podman_logout() { _complete_ "$options_with_args" "$boolean_options" } +_podman_healtcheck_run() { + local options_with_args="" + + local boolean_options=" + -h + --help + " + + case "$cur" in + -*) + COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) + ;; + *) + COMPREPLY=( $( compgen -W " + $(__podman_containers --all) + " -- "$cur" ) ) + __ltrim_colon_completions "$cur" + ;; + esac +} + _podman_generate_kube() { local options_with_args="" @@ -2979,6 +3020,7 @@ _podman_podman() { exec export generate + healthcheck history image images diff --git a/docs/podman-healthcheck-run.1.md b/docs/podman-healthcheck-run.1.md new file mode 100644 index 000000000..8cecb8eaa --- /dev/null +++ b/docs/podman-healthcheck-run.1.md @@ -0,0 +1,39 @@ +% podman-healthcheck-run(1) + +## NAME +podman\-healthcheck\-run- Run a container healthcheck + +## SYNOPSIS +**podman healthcheck run** [*options*] *container* + +## DESCRIPTION + +Runs the healthcheck command defined in a running container manually. The resulting error codes are defined +as follows: + +* 0 = healthcheck command succeeded +* 1 = healthcheck command failed +* 125 = an error has occurred + +Possible errors that can occur during the healthcheck are: +* unable to find the container +* container has no defined healthcheck +* container is not running + +## OPTIONS +**--help** + +Print usage statement + + +## EXAMPLES + +``` +$ podman healtcheck run mywebapp +``` + +## SEE ALSO +podman-healthcheck(1) + +## HISTORY +Feb 2019, Originally compiled by Brent Baude <bbaude@redhat.com> diff --git a/docs/podman-healthcheck.1.md b/docs/podman-healthcheck.1.md new file mode 100644 index 000000000..68d403231 --- /dev/null +++ b/docs/podman-healthcheck.1.md @@ -0,0 +1,22 @@ +% podman-healthcheck(1) + +## NAME +podman\-healthcheck- Manage healthchecks for containers + +## SYNOPSIS +**podman healthcheck** *subcommand* + +## DESCRIPTION +podman healthcheck is a set of subcommands that manage container healthchecks + +## SUBCOMMANDS + +| Command | Man Page | Description | +| ------- | ------------------------------------------------- | ------------------------------------------------------------------------------ | +| run | [podman-healthcheck-run(1)](podman-healthcheck-run.1.md) | Run a container healthcheck | + +## SEE ALSO +podman(1) + +## HISTORY +Feb 2019, Originally compiled by Brent Baude <bbaude@redhat.com> diff --git a/libpod/container.go b/libpod/container.go index 75f4a4a4f..2381f53ad 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -10,6 +10,7 @@ import ( "github.com/containernetworking/cni/pkg/types" cnitypes "github.com/containernetworking/cni/pkg/types/current" + "github.com/containers/image/manifest" "github.com/containers/libpod/libpod/lock" "github.com/containers/libpod/pkg/namespaces" "github.com/containers/storage" @@ -365,6 +366,9 @@ type ContainerConfig struct { // Systemd tells libpod to setup the container in systemd mode Systemd bool `json:"systemd"` + + // HealtchCheckConfig has the health check command and related timings + HealthCheckConfig *manifest.Schema2HealthConfig } // ContainerStatus returns a string representation for users @@ -1085,3 +1089,14 @@ func (c *Container) ContainerState() (*ContainerState, error) { deepcopier.Copy(c.state).To(returnConfig) return c.state, nil } + +// HasHealthCheck returns bool as to whether there is a health check +// defined for the container +func (c *Container) HasHealthCheck() bool { + return c.config.HealthCheckConfig != nil +} + +// HealthCheckConfig returns the command and timing attributes of the health check +func (c *Container) HealthCheckConfig() *manifest.Schema2HealthConfig { + return c.config.HealthCheckConfig +} diff --git a/libpod/healthcheck.go b/libpod/healthcheck.go new file mode 100644 index 000000000..81addb9a8 --- /dev/null +++ b/libpod/healthcheck.go @@ -0,0 +1,92 @@ +package libpod + +import ( + "os" + "strings" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// HealthCheckStatus represents the current state of a container +type HealthCheckStatus int + +const ( + // HealthCheckSuccess means the health worked + HealthCheckSuccess HealthCheckStatus = iota + // HealthCheckFailure means the health ran and failed + HealthCheckFailure HealthCheckStatus = iota + // HealthCheckContainerStopped means the health check cannot + // be run because the container is stopped + HealthCheckContainerStopped HealthCheckStatus = iota + // HealthCheckContainerNotFound means the container could + // not be found in local store + HealthCheckContainerNotFound HealthCheckStatus = iota + // HealthCheckNotDefined means the container has no health + // check defined in it + HealthCheckNotDefined HealthCheckStatus = iota + // HealthCheckInternalError means somes something failed obtaining or running + // a given health check + HealthCheckInternalError HealthCheckStatus = iota + // HealthCheckDefined means the healthcheck was found on the container + HealthCheckDefined HealthCheckStatus = iota +) + +// HealthCheck verifies the state and validity of the healthcheck configuration +// on the container and then executes the healthcheck +func (r *Runtime) HealthCheck(name string) (HealthCheckStatus, error) { + container, err := r.LookupContainer(name) + if err != nil { + return HealthCheckContainerNotFound, errors.Wrapf(err, "unable to lookup %s to perform a health check", name) + } + hcStatus, err := checkHealthCheckCanBeRun(container) + if err == nil { + return container.RunHealthCheck() + } + return hcStatus, err +} + +// RunHealthCheck runs the health check as defined by the container +func (c *Container) RunHealthCheck() (HealthCheckStatus, error) { + var newCommand []string + hcStatus, err := checkHealthCheckCanBeRun(c) + if err != nil { + return hcStatus, err + } + hcCommand := c.HealthCheckConfig().Test + if len(hcCommand) > 0 && hcCommand[0] == "CMD-SHELL" { + newCommand = []string{"sh", "-c"} + newCommand = append(newCommand, hcCommand[1:]...) + } else { + newCommand = hcCommand + } + // TODO when history/logging is implemented for healthcheck, we need to change the output streams + // so we can capture i/o + streams := new(AttachStreams) + streams.OutputStream = os.Stdout + streams.ErrorStream = os.Stderr + streams.InputStream = os.Stdin + streams.AttachOutput = true + streams.AttachError = true + streams.AttachInput = true + + logrus.Debugf("executing health check command %s for %s", strings.Join(newCommand, " "), c.ID()) + if err := c.Exec(false, false, []string{}, newCommand, "", "", streams, 0); err != nil { + return HealthCheckFailure, err + } + return HealthCheckSuccess, nil +} + +func checkHealthCheckCanBeRun(c *Container) (HealthCheckStatus, error) { + cstate, err := c.State() + if err != nil { + return HealthCheckInternalError, err + } + if cstate != ContainerStateRunning { + return HealthCheckContainerStopped, errors.Errorf("container %s is not running", c.ID()) + } + if !c.HasHealthCheck() { + return HealthCheckNotDefined, errors.Errorf("container %s has no defined healthcheck", c.ID()) + } + return HealthCheckDefined, nil +} diff --git a/libpod/image/image.go b/libpod/image/image.go index b20419d7b..8c98de3d3 100644 --- a/libpod/image/image.go +++ b/libpod/image/image.go @@ -1151,3 +1151,32 @@ func (i *Image) Save(ctx context.Context, source, format, output string, moreTag return nil } + +// GetConfigBlob returns a schema2image. If the image is not a schema2, then +// it will return an error +func (i *Image) GetConfigBlob(ctx context.Context) (*manifest.Schema2Image, error) { + imageRef, err := i.toImageRef(ctx) + if err != nil { + return nil, err + } + b, err := imageRef.ConfigBlob(ctx) + if err != nil { + return nil, errors.Wrapf(err, "unable to get config blob for %s", i.ID()) + } + blob := manifest.Schema2Image{} + if err := json.Unmarshal(b, &blob); err != nil { + return nil, errors.Wrapf(err, "unable to parse image blob for %s", i.ID()) + } + return &blob, nil + +} + +// GetHealthCheck returns a HealthConfig for an image. This function only works with +// schema2 images. +func (i *Image) GetHealthCheck(ctx context.Context) (*manifest.Schema2HealthConfig, error) { + configBlob, err := i.GetConfigBlob(ctx) + if err != nil { + return nil, err + } + return configBlob.ContainerConfig.Healthcheck, nil +} diff --git a/libpod/options.go b/libpod/options.go index 1e8592a25..5ad2824d9 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -7,6 +7,7 @@ import ( "regexp" "syscall" + "github.com/containers/image/manifest" "github.com/containers/libpod/pkg/namespaces" "github.com/containers/storage" "github.com/containers/storage/pkg/idtools" @@ -1469,3 +1470,14 @@ func WithInfraContainerPorts(bindings []ocicni.PortMapping) PodCreateOption { return nil } } + +// WithHealthCheck adds the healthcheck to the container config +func WithHealthCheck(healthCheck *manifest.Schema2HealthConfig) CtrCreateOption { + return func(ctr *Container) error { + if ctr.valid { + return ErrCtrFinalized + } + ctr.config.HealthCheckConfig = healthCheck + return nil + } +} diff --git a/pkg/adapter/runtime.go b/pkg/adapter/runtime.go index 5be2ca150..732b89530 100644 --- a/pkg/adapter/runtime.go +++ b/pkg/adapter/runtime.go @@ -332,3 +332,8 @@ func IsImageNotFound(err error) bool { } return false } + +// HealthCheck is a wrapper to same named function in libpod +func (r *LocalRuntime) HealthCheck(c *cliconfig.HealthCheckValues) (libpod.HealthCheckStatus, error) { + return r.Runtime.HealthCheck(c.InputArgs[0]) +} diff --git a/pkg/adapter/runtime_remote.go b/pkg/adapter/runtime_remote.go index 14cda7bff..10c25c3f3 100644 --- a/pkg/adapter/runtime_remote.go +++ b/pkg/adapter/runtime_remote.go @@ -746,3 +746,8 @@ func IsImageNotFound(err error) bool { } return false } + +// HealthCheck executes a container's healthcheck over a varlink connection +func (r *LocalRuntime) HealthCheck(c *cliconfig.HealthCheckValues) (libpod.HealthCheckStatus, error) { + return -1, libpod.ErrNotImplemented +} diff --git a/pkg/spec/createconfig.go b/pkg/spec/createconfig.go index 31039bfdf..6e7b297d2 100644 --- a/pkg/spec/createconfig.go +++ b/pkg/spec/createconfig.go @@ -9,6 +9,7 @@ import ( "strings" "syscall" + "github.com/containers/image/manifest" "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/namespaces" "github.com/containers/libpod/pkg/rootless" @@ -86,6 +87,8 @@ type CreateConfig struct { Env map[string]string //env ExposedPorts map[nat.Port]struct{} GroupAdd []string // group-add + HasHealthCheck bool + HealthCheck *manifest.Schema2HealthConfig HostAdd []string //add-host Hostname string //hostname Image string @@ -559,6 +562,10 @@ func (c *CreateConfig) GetContainerCreateOptions(runtime *libpod.Runtime, pod *l // Always use a cleanup process to clean up Podman after termination options = append(options, libpod.WithExitCommand(c.createExitCommand())) + if c.HasHealthCheck { + options = append(options, libpod.WithHealthCheck(c.HealthCheck)) + logrus.Debugf("New container has a health check") + } return options, nil } diff --git a/test/e2e/config.go b/test/e2e/config.go index 8116d993b..3fdb9e116 100644 --- a/test/e2e/config.go +++ b/test/e2e/config.go @@ -6,4 +6,5 @@ var ( ALPINE = "docker.io/library/alpine:latest" infra = "k8s.gcr.io/pause:3.1" BB = "docker.io/library/busybox:latest" + healthcheck = "docker.io/libpod/alpine_healthcheck:latest" ) diff --git a/test/e2e/config_amd64.go b/test/e2e/config_amd64.go index 3459bea6d..d02de7a6e 100644 --- a/test/e2e/config_amd64.go +++ b/test/e2e/config_amd64.go @@ -3,7 +3,7 @@ package integration var ( STORAGE_OPTIONS = "--storage-driver vfs" ROOTLESS_STORAGE_OPTIONS = "--storage-driver vfs" - CACHE_IMAGES = []string{ALPINE, BB, fedoraMinimal, nginx, redis, registry, infra, labels} + CACHE_IMAGES = []string{ALPINE, BB, fedoraMinimal, nginx, redis, registry, infra, labels, healthcheck} nginx = "quay.io/libpod/alpine_nginx:latest" BB_GLIBC = "docker.io/library/busybox:glibc" registry = "docker.io/library/registry:2" diff --git a/test/e2e/healthcheck_run_test.go b/test/e2e/healthcheck_run_test.go new file mode 100644 index 000000000..921d325c3 --- /dev/null +++ b/test/e2e/healthcheck_run_test.go @@ -0,0 +1,85 @@ +// +build !remoteclient + +package integration + +import ( + "fmt" + "os" + + . "github.com/containers/libpod/test/utils" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Podman healthcheck run", func() { + var ( + tempdir string + err error + podmanTest *PodmanTestIntegration + ) + + BeforeEach(func() { + tempdir, err = CreateTempDirInTempDir() + if err != nil { + os.Exit(1) + } + podmanTest = PodmanTestCreate(tempdir) + podmanTest.RestoreAllArtifacts() + }) + + AfterEach(func() { + podmanTest.Cleanup() + f := CurrentGinkgoTestDescription() + timedResult := fmt.Sprintf("Test: %s completed in %f seconds", f.TestText, f.Duration.Seconds()) + GinkgoWriter.Write([]byte(timedResult)) + + }) + + It("podman healthcheck run bogus container", func() { + session := podmanTest.Podman([]string{"healthcheck", "run", "foobar"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Not(Equal(0))) + }) + + It("podman healthcheck on valid container", func() { + podmanTest.RestoreArtifact(healthcheck) + session := podmanTest.Podman([]string{"run", "-dt", "--name", "hc", healthcheck}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + hc := podmanTest.Podman([]string{"healthcheck", "run", "hc"}) + hc.WaitWithDefaultTimeout() + Expect(hc.ExitCode()).To(Equal(0)) + }) + + It("podman healthcheck that should fail", func() { + session := podmanTest.Podman([]string{"run", "-dt", "--name", "hc", "docker.io/libpod/badhealthcheck:latest"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + hc := podmanTest.Podman([]string{"healthcheck", "run", "hc"}) + hc.WaitWithDefaultTimeout() + Expect(hc.ExitCode()).To(Equal(1)) + }) + + It("podman healthcheck on stopped container", func() { + podmanTest.RestoreArtifact(healthcheck) + session := podmanTest.Podman([]string{"run", "-dt", "--name", "hc", healthcheck, "ls"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + hc := podmanTest.Podman([]string{"healthcheck", "run", "hc"}) + hc.WaitWithDefaultTimeout() + Expect(hc.ExitCode()).To(Equal(125)) + }) + + It("podman healthcheck on container without healthcheck", func() { + session := podmanTest.Podman([]string{"run", "-dt", "--name", "hc", ALPINE, "top"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + hc := podmanTest.Podman([]string{"healthcheck", "run", "hc"}) + hc.WaitWithDefaultTimeout() + Expect(hc.ExitCode()).To(Equal(125)) + }) +}) diff --git a/transfer.md b/transfer.md index c2d472f08..eec63d146 100644 --- a/transfer.md +++ b/transfer.md @@ -112,6 +112,7 @@ The following podman commands do not have a Docker equivalent: * [`podman container refresh`](/docs/podman-container-refresh.1.md) * [`podman container runlabel`](/docs/podman-container-runlabel.1.md) * [`podman container restore`](/docs/podman-container-restore.1.md) +* [`podman healthcheck run`](/docs/podman-healthcheck-run.1.md) * [`podman image exists`](./docs/podman-image-exists.1.md) * [`podman image sign`](./docs/podman-image-sign.1.md) * [`podman image trust`](./docs/podman-image-trust.1.md) |