podman healthcheck run (phase 1)

Add the ability to manually run a container's healthcheck command.
This is only the first phase of implementing the healthcheck.
Subsequent pull requests will deal with the exposing the results and
history of healthchecks as well as the scheduling.

Signed-off-by: baude <bbaude@redhat.com>
This commit is contained in:
baude
2019-02-26 20:54:57 -06:00
parent 645426fe79
commit 598bde52d0
20 changed files with 475 additions and 1 deletions

View File

@ -217,6 +217,10 @@ type PauseValues struct {
All bool All bool
} }
type HealthCheckValues struct {
PodmanCommand
}
type KubePlayValues struct { type KubePlayValues struct {
PodmanCommand PodmanCommand
Authfile string Authfile string

View File

@ -121,3 +121,10 @@ func getSystemSubCommands() []*cobra.Command {
_renumberCommand, _renumberCommand,
} }
} }
// Commands that the local client implements
func getHealtcheckSubCommands() []*cobra.Command {
return []*cobra.Command{
_healthcheckrunCommand,
}
}

View File

@ -52,3 +52,8 @@ func getTrustSubCommands() []*cobra.Command {
func getSystemSubCommands() []*cobra.Command { func getSystemSubCommands() []*cobra.Command {
return []*cobra.Command{} return []*cobra.Command{}
} }
// Commands that the remoteclient implements
func getHealtcheckSubCommands() []*cobra.Command {
return []*cobra.Command{}
}

View File

@ -12,6 +12,7 @@ import (
"strings" "strings"
"syscall" "syscall"
"github.com/containers/image/manifest"
"github.com/containers/libpod/cmd/podman/cliconfig" "github.com/containers/libpod/cmd/podman/cliconfig"
"github.com/containers/libpod/cmd/podman/libpodruntime" "github.com/containers/libpod/cmd/podman/libpodruntime"
"github.com/containers/libpod/cmd/podman/shared" "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) { func createContainer(c *cliconfig.PodmanCommand, runtime *libpod.Runtime) (*libpod.Container, *cc.CreateConfig, error) {
var (
hasHealthCheck bool
healthCheck *manifest.Schema2HealthConfig
)
if c.Bool("trace") { if c.Bool("trace") {
span, _ := opentracing.StartSpanFromContext(Ctx, "createContainer") span, _ := opentracing.StartSpanFromContext(Ctx, "createContainer")
defer span.Finish() defer span.Finish()
@ -163,12 +168,32 @@ func createContainer(c *cliconfig.PodmanCommand, runtime *libpod.Runtime) (*libp
} else { } else {
imageName = newImage.ID() 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) createConfig, err := parseCreateOpts(ctx, c, runtime, imageName, data)
if err != nil { if err != nil {
return nil, nil, err 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) ctr, err := createContainerFromCreateConfig(runtime, createConfig, ctx, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err

25
cmd/podman/healthcheck.go Normal file
View File

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

View File

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

View File

@ -888,6 +888,26 @@ _podman_container_wait() {
_podman_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() { _podman_generate() {
local boolean_options=" local boolean_options="
--help --help
@ -2338,6 +2358,27 @@ _podman_logout() {
_complete_ "$options_with_args" "$boolean_options" _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() { _podman_generate_kube() {
local options_with_args="" local options_with_args=""
@ -2979,6 +3020,7 @@ _podman_podman() {
exec exec
export export
generate generate
healthcheck
history history
image image
images images

View File

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

View File

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

View File

@ -10,6 +10,7 @@ import (
"github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types"
cnitypes "github.com/containernetworking/cni/pkg/types/current" cnitypes "github.com/containernetworking/cni/pkg/types/current"
"github.com/containers/image/manifest"
"github.com/containers/libpod/libpod/lock" "github.com/containers/libpod/libpod/lock"
"github.com/containers/libpod/pkg/namespaces" "github.com/containers/libpod/pkg/namespaces"
"github.com/containers/storage" "github.com/containers/storage"
@ -365,6 +366,9 @@ type ContainerConfig struct {
// Systemd tells libpod to setup the container in systemd mode // Systemd tells libpod to setup the container in systemd mode
Systemd bool `json:"systemd"` Systemd bool `json:"systemd"`
// HealtchCheckConfig has the health check command and related timings
HealthCheckConfig *manifest.Schema2HealthConfig
} }
// ContainerStatus returns a string representation for users // ContainerStatus returns a string representation for users
@ -1085,3 +1089,14 @@ func (c *Container) ContainerState() (*ContainerState, error) {
deepcopier.Copy(c.state).To(returnConfig) deepcopier.Copy(c.state).To(returnConfig)
return c.state, nil 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
}

92
libpod/healthcheck.go Normal file
View File

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

View File

@ -1151,3 +1151,32 @@ func (i *Image) Save(ctx context.Context, source, format, output string, moreTag
return nil 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
}

View File

@ -7,6 +7,7 @@ import (
"regexp" "regexp"
"syscall" "syscall"
"github.com/containers/image/manifest"
"github.com/containers/libpod/pkg/namespaces" "github.com/containers/libpod/pkg/namespaces"
"github.com/containers/storage" "github.com/containers/storage"
"github.com/containers/storage/pkg/idtools" "github.com/containers/storage/pkg/idtools"
@ -1469,3 +1470,14 @@ func WithInfraContainerPorts(bindings []ocicni.PortMapping) PodCreateOption {
return nil 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
}
}

View File

@ -332,3 +332,8 @@ func IsImageNotFound(err error) bool {
} }
return false 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])
}

View File

@ -746,3 +746,8 @@ func IsImageNotFound(err error) bool {
} }
return false 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
}

View File

@ -9,6 +9,7 @@ import (
"strings" "strings"
"syscall" "syscall"
"github.com/containers/image/manifest"
"github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod"
"github.com/containers/libpod/pkg/namespaces" "github.com/containers/libpod/pkg/namespaces"
"github.com/containers/libpod/pkg/rootless" "github.com/containers/libpod/pkg/rootless"
@ -86,6 +87,8 @@ type CreateConfig struct {
Env map[string]string //env Env map[string]string //env
ExposedPorts map[nat.Port]struct{} ExposedPorts map[nat.Port]struct{}
GroupAdd []string // group-add GroupAdd []string // group-add
HasHealthCheck bool
HealthCheck *manifest.Schema2HealthConfig
HostAdd []string //add-host HostAdd []string //add-host
Hostname string //hostname Hostname string //hostname
Image string 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 // Always use a cleanup process to clean up Podman after termination
options = append(options, libpod.WithExitCommand(c.createExitCommand())) 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 return options, nil
} }

View File

@ -6,4 +6,5 @@ var (
ALPINE = "docker.io/library/alpine:latest" ALPINE = "docker.io/library/alpine:latest"
infra = "k8s.gcr.io/pause:3.1" infra = "k8s.gcr.io/pause:3.1"
BB = "docker.io/library/busybox:latest" BB = "docker.io/library/busybox:latest"
healthcheck = "docker.io/libpod/alpine_healthcheck:latest"
) )

View File

@ -3,7 +3,7 @@ package integration
var ( var (
STORAGE_OPTIONS = "--storage-driver vfs" STORAGE_OPTIONS = "--storage-driver vfs"
ROOTLESS_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" nginx = "quay.io/libpod/alpine_nginx:latest"
BB_GLIBC = "docker.io/library/busybox:glibc" BB_GLIBC = "docker.io/library/busybox:glibc"
registry = "docker.io/library/registry:2" registry = "docker.io/library/registry:2"

View File

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

View File

@ -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 refresh`](/docs/podman-container-refresh.1.md)
* [`podman container runlabel`](/docs/podman-container-runlabel.1.md) * [`podman container runlabel`](/docs/podman-container-runlabel.1.md)
* [`podman container restore`](/docs/podman-container-restore.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 exists`](./docs/podman-image-exists.1.md)
* [`podman image sign`](./docs/podman-image-sign.1.md) * [`podman image sign`](./docs/podman-image-sign.1.md)
* [`podman image trust`](./docs/podman-image-trust.1.md) * [`podman image trust`](./docs/podman-image-trust.1.md)