diff options
146 files changed, 3086 insertions, 740 deletions
diff --git a/.cirrus.yml b/.cirrus.yml index b473980ea..328b2e676 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -27,11 +27,17 @@ env: #### #### Cache-image names to test with ### + ACTIVE_CACHE_IMAGE_NAMES: >- + fedora-29-libpod-d6d53e40 + fedora-28-libpod-d6d53e40 + ubuntu-18-libpod-d6d53e40 + rhel-7-libpod-7f4cd1f7 + image-builder-image-1541772081 FEDORA_CACHE_IMAGE_NAME: "fedora-29-libpod-d6d53e40" PRIOR_FEDORA_CACHE_IMAGE_NAME: "fedora-28-libpod-d6d53e40" UBUNTU_CACHE_IMAGE_NAME: "ubuntu-18-libpod-d6d53e40" + PRIOR_RHEL_CACHE_IMAGE_NAME: "rhel-7-libpod-7f4cd1f7" # RHEL_CACHE_IMAGE_NAME: "rhel-8-notready" - PRIOR_RHEL_CACHE_IMAGE_NAME: "rhel-7-libpod-d6d53e40" # CENTOS_CACHE_IMAGE_NAME: "centos-7-notready" #### @@ -108,6 +114,7 @@ gating_task: gate_script: - '/usr/local/bin/entrypoint.sh validate' - '/usr/local/bin/entrypoint.sh lint' + - '${CIRRUS_WORKING_DIR}/${SCRIPT_BASE}/test/test_dot_cirrus_yaml.py' # This task runs `make vendor` followed by ./hack/tree_status.sh to check # whether the git tree is clean. The reasoning for that is to make sure @@ -149,6 +156,30 @@ build_each_commit_task: - env GOPATH=/var/tmp/go/ make build-all-new-commits GIT_BASE_BRANCH=origin/$CIRRUS_BASE_BRANCH +# Update metadata on VM images referenced by this repository state +meta_task: + + depends_on: + - "gating" + + container: + image: "quay.io/libpod/imgts:latest" # see contrib/imgts + cpu: 1 + memory: 1 + + env: + # Space-separated list of images used by this repository state + IMGNAMES: "${ACTIVE_CACHE_IMAGE_NAMES}" + BUILDID: "${CIRRUS_BUILD_ID}" + REPOREF: "${CIRRUS_CHANGE_IN_REPO}" + GCPJSON: ENCRYPTED[950d9c64ad78f7b1f0c7e499b42dc058d2b23aa67e38b315e68f557f2aba0bf83068d4734f7b1e1bdd22deabe99629df] + GCPNAME: ENCRYPTED[b05d469a0dba8cb479cb00cc7c1f6747c91d17622fba260a986b976aa6c817d4077eacffd4613d6d5f23afc4084fab1d] + GCPPROJECT: ENCRYPTED[7c80e728e046b1c76147afd156a32c1c57d4a1ac1eab93b7e68e718c61ca8564fc61fef815952b8ae0a64e7034b8fe4f] + CIRRUS_CLONE_DEPTH: 1 # source not used + + script: /usr/local/bin/entrypoint.sh + + # This task does the unit and integration testing for every platform testing_task: @@ -143,7 +143,7 @@ in the [API.md](https://github.com/containers/libpod/blob/master/API.md) file in [func VolumesPrune() []string, []string](#VolumesPrune) -[func WaitContainer(name: string) int](#WaitContainer) +[func WaitContainer(name: string, interval: int) int](#WaitContainer) [type BuildInfo](#BuildInfo) @@ -1013,10 +1013,10 @@ VolumesPrune removes unused volumes on the host ### <a name="WaitContainer"></a>func WaitContainer <div style="background-color: #E8E8E8; padding: 15px; margin: 10px; border-radius: 10px;"> -method WaitContainer(name: [string](https://godoc.org/builtin#string)) [int](https://godoc.org/builtin#int)</div> -WaitContainer takes the name or ID of a container and waits until the container stops. Upon stopping, the return -code of the container is returned. If the container container cannot be found by ID or name, -a [ContainerNotFound](#ContainerNotFound) error is returned. +method WaitContainer(name: [string](https://godoc.org/builtin#string), interval: [int](https://godoc.org/builtin#int)) [int](https://godoc.org/builtin#int)</div> +WaitContainer takes the name or ID of a container and waits the given interval in milliseconds until the container +stops. Upon stopping, the return code of the container is returned. If the container container cannot be found by ID +or name, a [ContainerNotFound](#ContainerNotFound) error is returned. ## Types ### <a name="BuildInfo"></a>type BuildInfo diff --git a/cmd/podman/attach.go b/cmd/podman/attach.go index a22aa92a1..08976cdaa 100644 --- a/cmd/podman/attach.go +++ b/cmd/podman/attach.go @@ -30,6 +30,7 @@ var ( func init() { attachCommand.Command = _attachCommand + attachCommand.SetHelpTemplate(HelpTemplate()) attachCommand.SetUsageTemplate(UsageTemplate()) flags := attachCommand.Flags() flags.StringVar(&attachCommand.DetachKeys, "detach-keys", "", "Override the key sequence for detaching a container. Format is a single character [a-Z] or ctrl-<value> where <value> is one of: a-z, @, ^, [, , or _") diff --git a/cmd/podman/build.go b/cmd/podman/build.go index cfeabfb4e..5fcf03b93 100644 --- a/cmd/podman/build.go +++ b/cmd/podman/build.go @@ -18,8 +18,7 @@ import ( var ( buildCommand cliconfig.BuildValues - buildDescription = "Builds an OCI or Docker image using instructions from one\n" + - "or more Dockerfiles and a specified build context directory." + buildDescription = "Builds an OCI or Docker image using instructions from one or more Dockerfiles and a specified build context directory." layerValues buildahcli.LayerResults budFlagsValues buildahcli.BudResults fromAndBudValues buildahcli.FromAndBudResults @@ -48,6 +47,7 @@ var ( func init() { buildCommand.Command = _buildCommand + buildCommand.SetHelpTemplate(HelpTemplate()) buildCommand.SetUsageTemplate(UsageTemplate()) flags := buildCommand.Flags() flags.SetInterspersed(false) diff --git a/cmd/podman/checkpoint.go b/cmd/podman/checkpoint.go index 367065766..dbf72c2cd 100644 --- a/cmd/podman/checkpoint.go +++ b/cmd/podman/checkpoint.go @@ -40,6 +40,7 @@ var ( func init() { checkpointCommand.Command = _checkpointCommand + checkpointCommand.SetHelpTemplate(HelpTemplate()) checkpointCommand.SetUsageTemplate(UsageTemplate()) flags := checkpointCommand.Flags() diff --git a/cmd/podman/cleanup.go b/cmd/podman/cleanup.go index fbbd337a7..17e637da1 100644 --- a/cmd/podman/cleanup.go +++ b/cmd/podman/cleanup.go @@ -37,6 +37,7 @@ var ( func init() { cleanupCommand.Command = _cleanupCommand + cleanupCommand.SetHelpTemplate(HelpTemplate()) cleanupCommand.SetUsageTemplate(UsageTemplate()) flags := cleanupCommand.Flags() 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/cliconfig/create.go b/cmd/podman/cliconfig/create.go index b5ca1be9c..49ab3d827 100644 --- a/cmd/podman/cliconfig/create.go +++ b/cmd/podman/cliconfig/create.go @@ -23,4 +23,5 @@ type BuildValues struct { type CpValues struct { PodmanCommand + Extract bool } diff --git a/cmd/podman/commands.go b/cmd/podman/commands.go index c261e54e2..d37af70c1 100644 --- a/cmd/podman/commands.go +++ b/cmd/podman/commands.go @@ -35,7 +35,6 @@ func getMainCommands() []*cobra.Command { _topCommand, _umountCommand, _unpauseCommand, - _waitCommand, } if len(_varlinkCommand.Use) > 0 { @@ -85,14 +84,6 @@ func getContainerSubCommands() []*cobra.Command { } } -// Commands that the local client implements -func getPodSubCommands() []*cobra.Command { - return []*cobra.Command{ - _podStatsCommand, - _podTopCommand, - } -} - func getGenerateSubCommands() []*cobra.Command { return []*cobra.Command{ _containerKubeCommand, @@ -121,3 +112,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..9b09e7dbc 100644 --- a/cmd/podman/commands_remoteclient.go +++ b/cmd/podman/commands_remoteclient.go @@ -29,11 +29,6 @@ func getContainerSubCommands() []*cobra.Command { } // commands that only the remoteclient implements -func getPodSubCommands() []*cobra.Command { - return []*cobra.Command{} -} - -// commands that only the remoteclient implements func getGenerateSubCommands() []*cobra.Command { return []*cobra.Command{} } @@ -52,3 +47,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/commit.go b/cmd/podman/commit.go index 43c54c320..584ab6880 100644 --- a/cmd/podman/commit.go +++ b/cmd/podman/commit.go @@ -19,10 +19,7 @@ import ( var ( commitCommand cliconfig.CommitValues - commitDescription = `Create an image from a container's changes. - Optionally tag the image created, set the author with the --author flag, - set the commit message with the --message flag, - and make changes to the instructions with the --change flag.` + commitDescription = `Create an image from a container's changes. Optionally tag the image created, set the author with the --author flag, set the commit message with the --message flag, and make changes to the instructions with the --change flag.` _commitCommand = &cobra.Command{ Use: "commit [flags] CONTAINER IMAGE", @@ -41,6 +38,7 @@ var ( func init() { commitCommand.Command = _commitCommand + commitCommand.SetHelpTemplate(HelpTemplate()) commitCommand.SetUsageTemplate(UsageTemplate()) flags := commitCommand.Flags() flags.StringSliceVarP(&commitCommand.Change, "change", "c", []string{}, fmt.Sprintf("Apply the following possible instructions to the created image (default []): %s", strings.Join(libpod.ChangeCmds, " | "))) diff --git a/cmd/podman/common.go b/cmd/podman/common.go index c9e937825..30eaa95d8 100644 --- a/cmd/podman/common.go +++ b/cmd/podman/common.go @@ -3,7 +3,6 @@ package main import ( "context" "fmt" - "github.com/spf13/cobra" "os" "strings" @@ -14,6 +13,7 @@ import ( "github.com/containers/storage" "github.com/fatih/camelcase" "github.com/pkg/errors" + "github.com/spf13/cobra" ) var ( @@ -67,6 +67,16 @@ func noSubArgs(c *cobra.Command, args []string) error { return nil } +func commandRunE() func(*cobra.Command, []string) error { + return func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + return errors.Errorf("unrecognized command `%s %s`\nTry '%s --help' for more information.", cmd.CommandPath(), args[0], cmd.CommandPath()) + } else { + return errors.Errorf("missing command '%s COMMAND'\nTry '%s --help' for more information.", cmd.CommandPath(), cmd.CommandPath()) + } + } +} + // getAllOrLatestContainers tries to return the correct list of containers // depending if --all, --latest or <container-id> is used. // It requires the Context (c) and the Runtime (runtime). As different @@ -311,7 +321,7 @@ func getCreateFlags(c *cliconfig.PodmanCommand) { "kernel-memory", "", "Kernel memory limit (format: `<number>[<unit>]`, where unit = b, k, m or g)", ) - createFlags.StringSliceP( + createFlags.StringArrayP( "label", "l", []string{}, "Set metadata on container (default [])", ) @@ -521,11 +531,23 @@ func scrubServer(server string) string { return strings.TrimPrefix(server, "http://") } +// HelpTemplate returns the help template for podman commands +// This uses the short and long options. +// command should not use this. +func HelpTemplate() string { + return `{{.Short}} + +Description: + {{.Long}} + +{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}` +} + // UsageTemplate returns the usage template for podman commands // This blocks the desplaying of the global options. The main podman // command should not use this. func UsageTemplate() string { - return `Usage:{{if .Runnable}} + return `Usage:{{if (and .Runnable (not .HasAvailableSubCommands))}} {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}} diff --git a/cmd/podman/container.go b/cmd/podman/container.go index 0bcdf533a..8ad8d7a44 100644 --- a/cmd/podman/container.go +++ b/cmd/podman/container.go @@ -15,6 +15,7 @@ var ( Short: "Manage Containers", Long: containerDescription, TraverseChildren: true, + RunE: commandRunE(), }, } diff --git a/cmd/podman/containers_prune.go b/cmd/podman/containers_prune.go index 0d0e75332..39be70c5b 100644 --- a/cmd/podman/containers_prune.go +++ b/cmd/podman/containers_prune.go @@ -34,6 +34,7 @@ var ( func init() { pruneContainersCommand.Command = _pruneContainersCommand + pruneContainersCommand.SetHelpTemplate(HelpTemplate()) pruneContainersCommand.SetUsageTemplate(UsageTemplate()) flags := pruneContainersCommand.Flags() flags.BoolVarP(&pruneContainersCommand.Force, "force", "f", false, "Force removal of a running container. The default is false") diff --git a/cmd/podman/cp.go b/cmd/podman/cp.go index 5958370b6..6223676ac 100644 --- a/cmd/podman/cp.go +++ b/cmd/podman/cp.go @@ -27,8 +27,11 @@ import ( var ( cpCommand cliconfig.CpValues - cpDescription = "Copy files/folders between a container and the local filesystem" - _cpCommand = &cobra.Command{ + cpDescription = `Command copies the contents of SRC_PATH to the DEST_PATH. + + You can copy from the container's file system to the local machine or the reverse, from the local filesystem to the container. If "-" is specified for either the SRC_PATH or DEST_PATH, you can also stream a tar archive from STDIN or to STDOUT. The CONTAINER can be a running or stopped container. The SRC_PATH or DEST_PATH can be a file or directory. +` + _cpCommand = &cobra.Command{ Use: "cp [flags] SRC_PATH DEST_PATH", Short: "Copy files/folders between a container and the local filesystem", Long: cpDescription, @@ -43,6 +46,10 @@ var ( func init() { cpCommand.Command = _cpCommand + flags := cpCommand.Flags() + flags.BoolVar(&cpCommand.Extract, "extract", false, "Extract the tar file into the destination directory.") + cpCommand.SetHelpTemplate(HelpTemplate()) + cpCommand.SetUsageTemplate(UsageTemplate()) rootCmd.AddCommand(cpCommand.Command) } @@ -61,10 +68,11 @@ func cpCmd(c *cliconfig.CpValues) error { } defer runtime.Shutdown(false) - return copyBetweenHostAndContainer(runtime, args[0], args[1]) + extract := c.Flag("extract").Changed + return copyBetweenHostAndContainer(runtime, args[0], args[1], extract) } -func copyBetweenHostAndContainer(runtime *libpod.Runtime, src string, dest string) error { +func copyBetweenHostAndContainer(runtime *libpod.Runtime, src string, dest string, extract bool) error { srcCtr, srcPath := parsePath(runtime, src) destCtr, destPath := parsePath(runtime, dest) @@ -166,7 +174,7 @@ func copyBetweenHostAndContainer(runtime *libpod.Runtime, src string, dest strin var lastError error for _, src := range glob { - err := copy(src, destPath, dest, idMappingOpts, &containerOwner) + err := copy(src, destPath, dest, idMappingOpts, &containerOwner, extract) if lastError != nil { logrus.Error(lastError) } @@ -219,7 +227,7 @@ func getPathInfo(path string) (string, os.FileInfo, error) { return path, srcfi, nil } -func copy(src, destPath, dest string, idMappingOpts storage.IDMappingOptions, chownOpts *idtools.IDPair) error { +func copy(src, destPath, dest string, idMappingOpts storage.IDMappingOptions, chownOpts *idtools.IDPair, extract bool) error { srcPath, err := filepath.EvalSymlinks(src) if err != nil { return errors.Wrapf(err, "error evaluating symlinks %q", srcPath) @@ -240,6 +248,7 @@ func copy(src, destPath, dest string, idMappingOpts storage.IDMappingOptions, ch // return functions for copying items copyFileWithTar := chrootarchive.CopyFileWithTarAndChown(chownOpts, digest.Canonical.Digester().Hash(), idMappingOpts.UIDMap, idMappingOpts.GIDMap) copyWithTar := chrootarchive.CopyWithTarAndChown(chownOpts, digest.Canonical.Digester().Hash(), idMappingOpts.UIDMap, idMappingOpts.GIDMap) + untarPath := chrootarchive.UntarPathAndChown(chownOpts, digest.Canonical.Digester().Hash(), idMappingOpts.UIDMap, idMappingOpts.GIDMap) if srcfi.IsDir() { @@ -263,6 +272,15 @@ func copy(src, destPath, dest string, idMappingOpts storage.IDMappingOptions, ch destPath = filepath.Join(destPath, filepath.Base(srcPath)) } } + + if extract { + // We're extracting an archive into the destination directory. + logrus.Debugf("extracting contents of %q into %q", srcPath, destPath) + if err = untarPath(srcPath, destPath); err != nil { + return errors.Wrapf(err, "error extracting %q into %q", srcPath, destPath) + } + return nil + } // Copy the file, preserving attributes. logrus.Debugf("copying %q to %q", srcPath, destPath) if err = copyFileWithTar(srcPath, destPath); err != nil { diff --git a/cmd/podman/create.go b/cmd/podman/create.go index 129c886b2..8a5d0cf73 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" @@ -36,11 +37,9 @@ import ( var ( createCommand cliconfig.CreateValues - createDescription = "Creates a new container from the given image or" + - " storage and prepares it for running the specified command. The" + - " container ID is then printed to stdout. You can then start it at" + - " any time with the podman start <container_id> command. The container" + - " will be created with the initial state 'created'." + createDescription = `Creates a new container from the given image or storage and prepares it for running the specified command. + + The container ID is then printed to stdout. You can then start it at any time with the podman start <container_id> command. The container will be created with the initial state 'created'.` _createCommand = &cobra.Command{ Use: "create [flags] IMAGE [COMMAND [ARG...]]", Short: "Create but do not start a container", @@ -63,6 +62,7 @@ var ( func init() { createCommand.PodmanCommand.Command = _createCommand + createCommand.SetHelpTemplate(HelpTemplate()) createCommand.SetUsageTemplate(UsageTemplate()) getCreateFlags(&createCommand.PodmanCommand) @@ -117,6 +117,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 +167,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 @@ -568,7 +592,7 @@ func parseCreateOpts(ctx context.Context, c *cliconfig.PodmanCommand, runtime *l } // LABEL VARIABLES - labels, err := getAllLabels(c.StringSlice("label-file"), c.StringSlice("label")) + labels, err := getAllLabels(c.StringSlice("label-file"), c.StringArray("label")) if err != nil { return nil, errors.Wrapf(err, "unable to process labels") } @@ -869,7 +893,16 @@ func joinOrCreateRootlessUserNamespace(createConfig *cc.CreateConfig, runtime *l } return false, -1, errors.Errorf("dependency container %s is not running", ctr.ID()) } - return rootless.JoinNS(uint(pid), 0) + + data, err := ioutil.ReadFile(ctr.Config().ConmonPidFile) + if err != nil { + return false, -1, errors.Wrapf(err, "cannot read conmon PID file %q", ctr.Config().ConmonPidFile) + } + conmonPid, err := strconv.Atoi(string(data)) + if err != nil { + return false, -1, errors.Wrapf(err, "cannot parse PID %q", data) + } + return rootless.JoinDirectUserAndMountNS(uint(conmonPid)) } } return rootless.BecomeRootInUserNS() diff --git a/cmd/podman/diff.go b/cmd/podman/diff.go index e232d7e66..bd3a985b7 100644 --- a/cmd/podman/diff.go +++ b/cmd/podman/diff.go @@ -34,8 +34,7 @@ func (so stdoutStruct) Out() error { var ( diffCommand cliconfig.DiffValues - diffDescription = fmt.Sprint(`Displays changes on a container or image's filesystem. The - container or image will be compared to its parent layer`) + diffDescription = fmt.Sprint(`Displays changes on a container or image's filesystem. The container or image will be compared to its parent layer.`) _diffCommand = &cobra.Command{ Use: "diff [flags] CONTAINER | IMAGE", @@ -54,6 +53,7 @@ var ( func init() { diffCommand.Command = _diffCommand + diffCommand.SetHelpTemplate(HelpTemplate()) diffCommand.SetUsageTemplate(UsageTemplate()) flags := diffCommand.Flags() diff --git a/cmd/podman/exec.go b/cmd/podman/exec.go index 32a6e4bb5..e4cea1f5e 100644 --- a/cmd/podman/exec.go +++ b/cmd/podman/exec.go @@ -17,10 +17,7 @@ import ( var ( execCommand cliconfig.ExecValues - execDescription = ` - podman exec - - Run a command in a running container + execDescription = `Execute the specified command inside a running container. ` _execCommand = &cobra.Command{ Use: "exec [flags] CONTAINER [COMMAND [ARG...]]", @@ -39,6 +36,7 @@ var ( func init() { execCommand.Command = _execCommand + execCommand.SetHelpTemplate(HelpTemplate()) execCommand.SetUsageTemplate(UsageTemplate()) flags := execCommand.Flags() flags.SetInterspersed(false) @@ -108,16 +106,25 @@ func execCmd(c *cliconfig.ExecValues) error { } - pid, err := ctr.PID() - if err != nil { - return err - } - became, ret, err := rootless.JoinNS(uint(pid), c.PreserveFDs) - if err != nil { - return err - } - if became { - os.Exit(ret) + if os.Geteuid() != 0 { + var became bool + var ret int + + data, err := ioutil.ReadFile(ctr.Config().ConmonPidFile) + if err != nil { + return errors.Wrapf(err, "cannot read conmon PID file %q", ctr.Config().ConmonPidFile) + } + conmonPid, err := strconv.Atoi(string(data)) + if err != nil { + return errors.Wrapf(err, "cannot parse PID %q", data) + } + became, ret, err = rootless.JoinDirectUserAndMountNS(uint(conmonPid)) + if err != nil { + return err + } + if became { + os.Exit(ret) + } } // ENVIRONMENT VARIABLES diff --git a/cmd/podman/exists.go b/cmd/podman/exists.go index 109831e74..8a2f78c88 100644 --- a/cmd/podman/exists.go +++ b/cmd/podman/exists.go @@ -16,21 +16,12 @@ var ( containerExistsCommand cliconfig.ContainerExistsValues podExistsCommand cliconfig.PodExistsValues - imageExistsDescription = ` - podman image exists + imageExistsDescription = `If the named image exists in local storage, podman image exists exits with 0, otherwise the exit code will be 1.` - Check if an image exists in local storage -` - containerExistsDescription = ` - podman container exists + containerExistsDescription = `If the named container exists in local storage, podman container exists exits with 0, otherwise the exit code will be 1.` - Check if a container exists in local storage -` - podExistsDescription = ` - podman pod exists + podExistsDescription = `If the named pod exists in local storage, podman pod exists exits with 0, otherwise the exit code will be 1.` - Check if a pod exists in local storage -` _imageExistsCommand = &cobra.Command{ Use: "exists IMAGE", Short: "Check if an image exists in local storage", @@ -75,12 +66,15 @@ var ( func init() { imageExistsCommand.Command = _imageExistsCommand imageExistsCommand.DisableFlagsInUseLine = true + imageExistsCommand.SetHelpTemplate(HelpTemplate()) imageExistsCommand.SetUsageTemplate(UsageTemplate()) containerExistsCommand.Command = _containerExistsCommand containerExistsCommand.DisableFlagsInUseLine = true + containerExistsCommand.SetHelpTemplate(HelpTemplate()) containerExistsCommand.SetUsageTemplate(UsageTemplate()) podExistsCommand.Command = _podExistsCommand podExistsCommand.DisableFlagsInUseLine = true + podExistsCommand.SetHelpTemplate(HelpTemplate()) podExistsCommand.SetUsageTemplate(UsageTemplate()) } diff --git a/cmd/podman/export.go b/cmd/podman/export.go index d40c05019..4be2a3c86 100644 --- a/cmd/podman/export.go +++ b/cmd/podman/export.go @@ -32,6 +32,7 @@ var ( func init() { exportCommand.Command = _exportCommand + exportCommand.SetHelpTemplate(HelpTemplate()) exportCommand.SetUsageTemplate(UsageTemplate()) flags := exportCommand.Flags() flags.StringVarP(&exportCommand.Output, "output", "o", "/dev/stdout", "Write to a file, default is STDOUT") diff --git a/cmd/podman/generate.go b/cmd/podman/generate.go index 773d625ee..197fd26a6 100644 --- a/cmd/podman/generate.go +++ b/cmd/podman/generate.go @@ -12,6 +12,7 @@ var ( Use: "generate", Short: "Generated structured data", Long: generateDescription, + RunE: commandRunE(), } ) diff --git a/cmd/podman/generate_kube.go b/cmd/podman/generate_kube.go index fa2872b77..e3db14af3 100644 --- a/cmd/podman/generate_kube.go +++ b/cmd/podman/generate_kube.go @@ -15,10 +15,12 @@ import ( var ( containerKubeCommand cliconfig.GenerateKubeValues - containerKubeDescription = "Generate Kubernetes Pod YAML" - _containerKubeCommand = &cobra.Command{ + containerKubeDescription = `Command generates Kubernetes Pod YAML (v1 specification) from a podman container or pod. + + Whether the input is for a container or pod, Podman will always generate the specification as a Pod. The input may be in the form of a pod or container name or ID.` + _containerKubeCommand = &cobra.Command{ Use: "kube [flags] CONTAINER | POD", - Short: "Generate Kubernetes pod YAML for a container or pod", + Short: "Generate Kubernetes pod YAML from a container or pod", Long: containerKubeDescription, RunE: func(cmd *cobra.Command, args []string) error { containerKubeCommand.InputArgs = args @@ -33,6 +35,7 @@ var ( func init() { containerKubeCommand.Command = _containerKubeCommand + containerKubeCommand.SetHelpTemplate(HelpTemplate()) containerKubeCommand.SetUsageTemplate(UsageTemplate()) flags := containerKubeCommand.Flags() flags.BoolVarP(&containerKubeCommand.Service, "service", "s", false, "Generate YAML for kubernetes service object") diff --git a/cmd/podman/healthcheck.go b/cmd/podman/healthcheck.go new file mode 100644 index 000000000..48d6b6bbf --- /dev/null +++ b/cmd/podman/healthcheck.go @@ -0,0 +1,26 @@ +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, + RunE: commandRunE(), + }, +} + +// 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..d92b2ac01 --- /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 [flags] 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/cmd/podman/history.go b/cmd/podman/history.go index 533ee91cb..f6cfe91b6 100644 --- a/cmd/podman/history.go +++ b/cmd/podman/history.go @@ -37,8 +37,9 @@ type historyOptions struct { var ( historyCommand cliconfig.HistoryValues - historyDescription = "Displays the history of an image. The information can be printed out in an easy to read, " + - "or user specified format, and can be truncated." + historyDescription = `Displays the history of an image. + + The information can be printed out in an easy to read, or user specified format, and can be truncated.` _historyCommand = &cobra.Command{ Use: "history [flags] IMAGE", Short: "Show history of a specified image", @@ -53,6 +54,7 @@ var ( func init() { historyCommand.Command = _historyCommand + historyCommand.SetHelpTemplate(HelpTemplate()) historyCommand.SetUsageTemplate(UsageTemplate()) flags := historyCommand.Flags() flags.StringVar(&historyCommand.Format, "format", "", "Change the output to JSON or a Go template") diff --git a/cmd/podman/image.go b/cmd/podman/image.go index 57be7fe14..52bac6ecb 100644 --- a/cmd/podman/image.go +++ b/cmd/podman/image.go @@ -14,6 +14,7 @@ var ( Use: "image", Short: "Manage images", Long: imageDescription, + RunE: commandRunE(), }, } imagesSubCommand cliconfig.ImagesValues diff --git a/cmd/podman/imagefilters/filters.go b/cmd/podman/imagefilters/filters.go index d01eb7436..2932d61c0 100644 --- a/cmd/podman/imagefilters/filters.go +++ b/cmd/podman/imagefilters/filters.go @@ -2,11 +2,14 @@ package imagefilters import ( "context" + "fmt" + "path/filepath" "strings" "time" "github.com/containers/libpod/pkg/adapter" "github.com/containers/libpod/pkg/inspect" + "github.com/sirupsen/logrus" ) // ResultFilter is a mock function for image filtering @@ -61,6 +64,27 @@ func LabelFilter(ctx context.Context, labelfilter string) ResultFilter { } } +// ReferenceFilter allows you to filter by image name +// Replacing all '/' with '|' so that filepath.Match() can work +// '|' character is not valid in image name, so this is safe +func ReferenceFilter(ctx context.Context, referenceFilter string) ResultFilter { + filter := fmt.Sprintf("*%s*", referenceFilter) + filter = strings.Replace(filter, "/", "|", -1) + return func(i *adapter.ContainerImage) bool { + for _, name := range i.Names() { + newName := strings.Replace(name, "/", "|", -1) + match, err := filepath.Match(filter, newName) + if err != nil { + logrus.Errorf("failed to match %s and %s, %q", name, referenceFilter, err) + } + if match { + return true + } + } + return false + } +} + // OutputImageFilter allows you to filter by an a specific image name func OutputImageFilter(userImage *adapter.ContainerImage) ResultFilter { return func(i *adapter.ContainerImage) bool { diff --git a/cmd/podman/images.go b/cmd/podman/images.go index 26e51bef7..78dc87ad5 100644 --- a/cmd/podman/images.go +++ b/cmd/podman/images.go @@ -85,7 +85,7 @@ func (a imagesSortedSize) Less(i, j int) bool { var ( imagesCommand cliconfig.ImagesValues - imagesDescription = "lists locally stored images." + imagesDescription = "Lists images previously pulled to the system or created on the system." _imagesCommand = cobra.Command{ Use: "images [flags] [IMAGE]", @@ -103,6 +103,7 @@ var ( ) func imagesInit(command *cliconfig.ImagesValues) { + command.SetHelpTemplate(HelpTemplate()) command.SetUsageTemplate(UsageTemplate()) flags := command.Flags() @@ -375,6 +376,9 @@ func CreateFilterFuncs(ctx context.Context, r *adapter.LocalRuntime, filters []s case "label": labelFilter := strings.Join(splitFilter[1:], "=") filterFuncs = append(filterFuncs, imagefilters.LabelFilter(ctx, labelFilter)) + case "reference": + referenceFilter := strings.Join(splitFilter[1:], "=") + filterFuncs = append(filterFuncs, imagefilters.ReferenceFilter(ctx, referenceFilter)) default: return nil, errors.Errorf("invalid filter %s ", splitFilter[0]) } diff --git a/cmd/podman/images_prune.go b/cmd/podman/images_prune.go index 427374f68..b6f335fb3 100644 --- a/cmd/podman/images_prune.go +++ b/cmd/podman/images_prune.go @@ -11,11 +11,9 @@ import ( var ( pruneImagesCommand cliconfig.PruneImagesValues - pruneImagesDescription = ` - podman image prune + pruneImagesDescription = `Removes all unnamed images from local storage. - Removes all unnamed images from local storage -` + If an image is not being used by a container, it will be removed from the system.` _pruneImagesCommand = &cobra.Command{ Use: "prune", Args: noSubArgs, @@ -31,6 +29,7 @@ var ( func init() { pruneImagesCommand.Command = _pruneImagesCommand + pruneImagesCommand.SetHelpTemplate(HelpTemplate()) pruneImagesCommand.SetUsageTemplate(UsageTemplate()) flags := pruneImagesCommand.Flags() flags.BoolVarP(&pruneImagesCommand.All, "all", "a", false, "Remove all unused images, not just dangling ones") diff --git a/cmd/podman/import.go b/cmd/podman/import.go index ddf1bd802..c3351ab1b 100644 --- a/cmd/podman/import.go +++ b/cmd/podman/import.go @@ -13,9 +13,9 @@ var ( importCommand cliconfig.ImportValues importDescription = `Create a container image from the contents of the specified tarball (.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz). - Note remote tar balls can be specified, via web address. - Optionally tag the image. You can specify the instructions using the --change option. - ` + + Note remote tar balls can be specified, via web address. + Optionally tag the image. You can specify the instructions using the --change option.` _importCommand = &cobra.Command{ Use: "import [flags] PATH [REFERENCE]", Short: "Import a tarball to create a filesystem image", @@ -33,6 +33,7 @@ var ( func init() { importCommand.Command = _importCommand + importCommand.SetHelpTemplate(HelpTemplate()) importCommand.SetUsageTemplate(UsageTemplate()) flags := importCommand.Flags() flags.StringSliceVarP(&importCommand.Change, "change", "c", []string{}, "Apply the following possible instructions to the created image (default []): CMD | ENTRYPOINT | ENV | EXPOSE | LABEL | STOPSIGNAL | USER | VOLUME | WORKDIR") diff --git a/cmd/podman/info.go b/cmd/podman/info.go index e87f4e151..de20eb009 100644 --- a/cmd/podman/info.go +++ b/cmd/podman/info.go @@ -16,12 +16,15 @@ import ( var ( infoCommand cliconfig.InfoValues - infoDescription = "Display podman system information" - _infoCommand = &cobra.Command{ + infoDescription = `Display information pertaining to the host, current storage stats, and build of podman. + + Useful for the user and when reporting issues. +` + _infoCommand = &cobra.Command{ Use: "info", Args: noSubArgs, Long: infoDescription, - Short: `Display information pertaining to the host, current storage stats, and build of podman. Useful for the user and when reporting issues.`, + Short: "Display podman system information", RunE: func(cmd *cobra.Command, args []string) error { infoCommand.InputArgs = args infoCommand.GlobalFlags = MainGlobalOpts @@ -33,6 +36,7 @@ var ( func init() { infoCommand.Command = _infoCommand + infoCommand.SetHelpTemplate(HelpTemplate()) infoCommand.SetUsageTemplate(UsageTemplate()) flags := infoCommand.Flags() diff --git a/cmd/podman/inspect.go b/cmd/podman/inspect.go index 1c93a03e1..0af96088f 100644 --- a/cmd/podman/inspect.go +++ b/cmd/podman/inspect.go @@ -24,8 +24,10 @@ const ( var ( inspectCommand cliconfig.InspectValues - inspectDescription = "This displays the low-level information on containers and images identified by name or ID. By default, this will render all results in a JSON array. If the container and image have the same name, this will return container JSON for unspecified type." - _inspectCommand = &cobra.Command{ + inspectDescription = `This displays the low-level information on containers and images identified by name or ID. + + If given a name that matches both a container and an image, this command inspects the container. By default, this will render all results in a JSON array.` + _inspectCommand = &cobra.Command{ Use: "inspect [flags] CONTAINER | IMAGE", Short: "Display the configuration of a container or image", Long: inspectDescription, @@ -42,6 +44,7 @@ var ( func init() { inspectCommand.Command = _inspectCommand + inspectCommand.SetHelpTemplate(HelpTemplate()) inspectCommand.SetUsageTemplate(UsageTemplate()) flags := inspectCommand.Flags() flags.StringVarP(&inspectCommand.TypeObject, "type", "t", inspectAll, "Return JSON for specified type, (e.g image, container or task)") diff --git a/cmd/podman/kill.go b/cmd/podman/kill.go index 76d2516b7..2c1e13eaf 100644 --- a/cmd/podman/kill.go +++ b/cmd/podman/kill.go @@ -2,16 +2,15 @@ package main import ( "fmt" - "syscall" + "reflect" + + "github.com/containers/libpod/pkg/adapter" + "github.com/opentracing/opentracing-go" "github.com/containers/libpod/cmd/podman/cliconfig" - "github.com/containers/libpod/cmd/podman/libpodruntime" - "github.com/containers/libpod/cmd/podman/shared" - "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/rootless" "github.com/docker/docker/pkg/signal" "github.com/pkg/errors" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -39,6 +38,7 @@ var ( func init() { killCommand.Command = _killCommand + killCommand.SetHelpTemplate(HelpTemplate()) killCommand.SetUsageTemplate(UsageTemplate()) flags := killCommand.Flags() @@ -51,54 +51,44 @@ func init() { // killCmd kills one or more containers with a signal func killCmd(c *cliconfig.KillValues) error { - var ( - killFuncs []shared.ParallelWorkerInput - killSignal uint = uint(syscall.SIGTERM) - ) + if c.Bool("trace") { + span, _ := opentracing.StartSpanFromContext(Ctx, "killCmd") + defer span.Finish() + } + + // Check if the signalString provided by the user is valid + // Invalid signals will return err + killSignal, err := signal.ParseSignal(c.Signal) + if err != nil { + return err + } rootless.SetSkipStorageSetup(true) - runtime, err := libpodruntime.GetRuntime(&c.PodmanCommand) + runtime, err := adapter.GetRuntime(&c.PodmanCommand) if err != nil { return errors.Wrapf(err, "could not get runtime") } defer runtime.Shutdown(false) - if c.Signal != "" { - // Check if the signalString provided by the user is valid - // Invalid signals will return err - sysSignal, err := signal.ParseSignal(c.Signal) - if err != nil { - return err - } - killSignal = uint(sysSignal) - } - - containers, err := getAllOrLatestContainers(&c.PodmanCommand, runtime, libpod.ContainerStateRunning, "running") + ok, failures, err := runtime.KillContainers(getContext(), c, killSignal) if err != nil { - if len(containers) == 0 { - return err - } - fmt.Println(err.Error()) + return err } - for _, ctr := range containers { - con := ctr - f := func() error { - return con.Kill(killSignal) - } - - killFuncs = append(killFuncs, shared.ParallelWorkerInput{ - ContainerID: con.ID(), - ParallelFunc: f, - }) + for _, id := range ok { + fmt.Println(id) } - maxWorkers := shared.Parallelize("kill") - if c.GlobalIsSet("max-workers") { - maxWorkers = c.GlobalFlags.MaxWorks - } - logrus.Debugf("Setting maximum workers to %d", maxWorkers) + if len(failures) > 0 { + keys := reflect.ValueOf(failures).MapKeys() + lastKey := keys[len(keys)-1].String() + lastErr := failures[lastKey] + delete(failures, lastKey) - killErrors, errCount := shared.ParallelExecuteWorkerPool(maxWorkers, killFuncs) - return printParallelOutput(killErrors, errCount) + for _, err := range failures { + outputError(err) + } + return lastErr + } + return nil } diff --git a/cmd/podman/load.go b/cmd/podman/load.go index 5a0742aba..3c71e2f61 100644 --- a/cmd/podman/load.go +++ b/cmd/podman/load.go @@ -30,6 +30,7 @@ var ( func init() { loadCommand.Command = _loadCommand + loadCommand.SetHelpTemplate(HelpTemplate()) loadCommand.SetUsageTemplate(UsageTemplate()) flags := loadCommand.Flags() flags.StringVarP(&loadCommand.Input, "input", "i", "/dev/stdin", "Read from archive file, default is STDIN") diff --git a/cmd/podman/login.go b/cmd/podman/login.go index 48d4eefbc..43a7d246e 100644 --- a/cmd/podman/login.go +++ b/cmd/podman/login.go @@ -37,6 +37,7 @@ var ( func init() { loginCommand.Command = _loginCommand + loginCommand.SetHelpTemplate(HelpTemplate()) loginCommand.SetUsageTemplate(UsageTemplate()) flags := loginCommand.Flags() diff --git a/cmd/podman/logout.go b/cmd/podman/logout.go index 2a540ceba..268e6b44c 100644 --- a/cmd/podman/logout.go +++ b/cmd/podman/logout.go @@ -30,6 +30,7 @@ var ( func init() { logoutCommand.Command = _logoutCommand + logoutCommand.SetHelpTemplate(HelpTemplate()) logoutCommand.SetUsageTemplate(UsageTemplate()) flags := logoutCommand.Flags() flags.BoolVarP(&logoutCommand.All, "all", "a", false, "Remove the cached credentials for all registries in the auth file") diff --git a/cmd/podman/logs.go b/cmd/podman/logs.go index a02010eda..9df7281fc 100644 --- a/cmd/podman/logs.go +++ b/cmd/podman/logs.go @@ -15,8 +15,10 @@ import ( var ( logsCommand cliconfig.LogsValues - logsDescription = "The podman logs command batch-retrieves whatever logs are present for a container at the time of execution. This does not guarantee execution" + - "order when combined with podman run (i.e. your run may not have generated any logs at the time you execute podman logs" + logsDescription = `Retrieves logs for a container. + + This does not guarantee execution order when combined with podman run (i.e. your run may not have generated any logs at the time you execute podman logs. +` _logsCommand = &cobra.Command{ Use: "logs [flags] CONTAINER", Short: "Fetch the logs of a container", @@ -34,6 +36,7 @@ var ( func init() { logsCommand.Command = _logsCommand + logsCommand.SetHelpTemplate(HelpTemplate()) logsCommand.SetUsageTemplate(UsageTemplate()) flags := logsCommand.Flags() flags.BoolVar(&logsCommand.Details, "details", false, "Show extra details provided to the logs") diff --git a/cmd/podman/main.go b/cmd/podman/main.go index b3faf05c0..7d4b650a9 100644 --- a/cmd/podman/main.go +++ b/cmd/podman/main.go @@ -52,6 +52,7 @@ var mainCommands = []*cobra.Command{ _stopCommand, _tagCommand, _versionCommand, + _waitCommand, imageCommand.Command, systemCommand.Command, } @@ -81,9 +82,7 @@ var cmdsNotRequiringRootless = map[*cobra.Command]bool{ var rootCmd = &cobra.Command{ Use: "podman", Long: "manage pods and images", - RunE: func(cmd *cobra.Command, args []string) error { - return cmd.Help() - }, + RunE: commandRunE(), PersistentPreRunE: func(cmd *cobra.Command, args []string) error { return before(cmd, args) }, diff --git a/cmd/podman/mount.go b/cmd/podman/mount.go index 3a3432194..c5b7e2404 100644 --- a/cmd/podman/mount.go +++ b/cmd/podman/mount.go @@ -17,12 +17,11 @@ import ( var ( mountCommand cliconfig.MountValues - mountDescription = ` - podman mount - Lists all mounted containers mount points + mountDescription = `podman mount + Lists all mounted containers mount points if no container is specified - podman mount CONTAINER-NAME-OR-ID - Mounts the specified container and outputs the mountpoint + podman mount CONTAINER-NAME-OR-ID + Mounts the specified container and outputs the mountpoint ` _mountCommand = &cobra.Command{ @@ -42,6 +41,7 @@ var ( func init() { mountCommand.Command = _mountCommand + mountCommand.SetHelpTemplate(HelpTemplate()) mountCommand.SetUsageTemplate(UsageTemplate()) flags := mountCommand.Flags() flags.BoolVarP(&mountCommand.All, "all", "a", false, "Mount all containers") diff --git a/cmd/podman/pause.go b/cmd/podman/pause.go index 3a5b80359..fa4648128 100644 --- a/cmd/podman/pause.go +++ b/cmd/podman/pause.go @@ -14,12 +14,8 @@ import ( var ( pauseCommand cliconfig.PauseValues - pauseDescription = ` - podman pause - - Pauses one or more running containers. The container name or ID can be used. -` - _pauseCommand = &cobra.Command{ + pauseDescription = `Pauses one or more running containers. The container name or ID can be used.` + _pauseCommand = &cobra.Command{ Use: "pause [flags] CONTAINER [CONTAINER...]", Short: "Pause all the processes in one or more containers", Long: pauseDescription, @@ -36,6 +32,7 @@ var ( func init() { pauseCommand.Command = _pauseCommand + pauseCommand.SetHelpTemplate(HelpTemplate()) pauseCommand.SetUsageTemplate(UsageTemplate()) flags := pauseCommand.Flags() flags.BoolVarP(&pauseCommand.All, "all", "a", false, "Pause all running containers") diff --git a/cmd/podman/play.go b/cmd/podman/play.go index 495a1f170..95eae653e 100644 --- a/cmd/podman/play.go +++ b/cmd/podman/play.go @@ -12,11 +12,13 @@ var ( Use: "play", Short: "Play a pod", Long: playDescription, + RunE: commandRunE(), } ) func init() { playCommand.Command = _playCommand + playCommand.SetHelpTemplate(HelpTemplate()) playCommand.SetUsageTemplate(UsageTemplate()) playCommand.AddCommand(getPlaySubCommands()...) } diff --git a/cmd/podman/play_kube.go b/cmd/podman/play_kube.go index ac46ad5c6..980f3a09c 100644 --- a/cmd/podman/play_kube.go +++ b/cmd/podman/play_kube.go @@ -25,10 +25,17 @@ import ( "k8s.io/api/core/v1" ) +const ( + // https://kubernetes.io/docs/concepts/storage/volumes/#hostpath + createDirectoryPermission = 0755 +) + var ( playKubeCommand cliconfig.KubePlayValues - playKubeDescription = "Play a Pod and its containers based on a Kubrernetes YAML" - _playKubeCommand = &cobra.Command{ + playKubeDescription = `Command reads in a structured file of Kubernetes YAML. + + It creates the pod and containers described in the YAML. The containers within the pod are then started and the ID of the new Pod is output.` + _playKubeCommand = &cobra.Command{ Use: "kube [flags] KUBEFILE", Short: "Play a pod based on Kubernetes YAML", Long: playKubeDescription, @@ -44,6 +51,7 @@ var ( func init() { playKubeCommand.Command = _playKubeCommand + playKubeCommand.SetHelpTemplate(HelpTemplate()) playKubeCommand.SetUsageTemplate(UsageTemplate()) flags := playKubeCommand.Flags() flags.StringVar(&playKubeCommand.Authfile, "authfile", "", "Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json. Use REGISTRY_AUTH_FILE environment variable to override") @@ -144,12 +152,41 @@ func playKubeYAMLCmd(c *cliconfig.KubePlayValues) error { dockerRegistryOptions.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!c.TlsVerify) } + // map from name to mount point + volumes := make(map[string]string) + for _, volume := range podYAML.Spec.Volumes { + hostPath := volume.VolumeSource.HostPath + if hostPath == nil { + return errors.Errorf("HostPath is currently the only supported VolumeSource") + } + if hostPath.Type != nil { + switch *hostPath.Type { + case v1.HostPathDirectoryOrCreate: + if _, err := os.Stat(hostPath.Path); os.IsNotExist(err) { + if err := os.Mkdir(hostPath.Path, createDirectoryPermission); err != nil { + return errors.Errorf("Error creating HostPath %s at %s", volume.Name, hostPath.Path) + } + } + case v1.HostPathDirectory: + // do nothing here because we will verify the path exists in validateVolumeHostDir + break + default: + return errors.Errorf("Directories are the only supported HostPath type") + } + } + if err := validateVolumeHostDir(hostPath.Path); err != nil { + return errors.Wrapf(err, "Error in parsing HostPath in YAML") + } + fmt.Println(volume.Name) + volumes[volume.Name] = hostPath.Path + } + for _, container := range podYAML.Spec.Containers { newImage, err := runtime.ImageRuntime().New(ctx, container.Image, c.SignaturePolicy, c.Authfile, writer, &dockerRegistryOptions, image2.SigningOptions{}, false, nil) if err != nil { return err } - createConfig := kubeContainerToCreateConfig(container, runtime, newImage, namespaces) + createConfig, err := kubeContainerToCreateConfig(container, runtime, newImage, namespaces, volumes) if err != nil { return err } @@ -194,7 +231,7 @@ func getPodPorts(containers []v1.Container) []ocicni.PortMapping { } // kubeContainerToCreateConfig takes a v1.Container and returns a createconfig describing a container -func kubeContainerToCreateConfig(containerYAML v1.Container, runtime *libpod.Runtime, newImage *image2.Image, namespaces map[string]string) *createconfig.CreateConfig { +func kubeContainerToCreateConfig(containerYAML v1.Container, runtime *libpod.Runtime, newImage *image2.Image, namespaces map[string]string, volumes map[string]string) (*createconfig.CreateConfig, error) { var ( containerConfig createconfig.CreateConfig envs map[string]string @@ -239,6 +276,17 @@ func kubeContainerToCreateConfig(containerYAML v1.Container, runtime *libpod.Run for _, e := range containerYAML.Env { envs[e.Name] = e.Value } + + for _, volume := range containerYAML.VolumeMounts { + host_path, exists := volumes[volume.Name] + if !exists { + return nil, errors.Errorf("Volume mount %s specified for container but not configured in volumes", volume.Name) + } + if err := validateVolumeCtrDir(volume.MountPath); err != nil { + return nil, errors.Wrapf(err, "error in parsing MountPath") + } + containerConfig.Volumes = append(containerConfig.Volumes, fmt.Sprintf("%s:%s", host_path, volume.MountPath)) + } containerConfig.Env = envs - return &containerConfig + return &containerConfig, nil } diff --git a/cmd/podman/pod.go b/cmd/podman/pod.go index c1350bd4d..2d9bca21d 100644 --- a/cmd/podman/pod.go +++ b/cmd/podman/pod.go @@ -6,15 +6,14 @@ import ( ) var ( - podDescription = `Manage container pods. - -Pods are a group of one or more containers sharing the same network, pid and ipc namespaces.` + podDescription = `Pods are a group of one or more containers sharing the same network, pid and ipc namespaces.` ) var podCommand = cliconfig.PodmanCommand{ Command: &cobra.Command{ Use: "pod", Short: "Manage pods", Long: podDescription, + RunE: commandRunE(), }, } @@ -29,12 +28,14 @@ var podSubCommands = []*cobra.Command{ _podRestartCommand, _podRmCommand, _podStartCommand, + _podStatsCommand, _podStopCommand, + _podTopCommand, _podUnpauseCommand, } func init() { podCommand.AddCommand(podSubCommands...) - podCommand.AddCommand(getPodSubCommands()...) + podCommand.SetHelpTemplate(HelpTemplate()) podCommand.SetUsageTemplate(UsageTemplate()) } diff --git a/cmd/podman/pod_create.go b/cmd/podman/pod_create.go index 43c211b2c..d2b7da597 100644 --- a/cmd/podman/pod_create.go +++ b/cmd/podman/pod_create.go @@ -17,10 +17,9 @@ var ( DefaultKernelNamespaces = "cgroup,ipc,net,uts" podCreateCommand cliconfig.PodCreateValues - podCreateDescription = "Creates a new empty pod. The pod ID is then" + - " printed to stdout. You can then start it at any time with the" + - " podman pod start <pod_id> command. The pod will be created with the" + - " initial state 'created'." + podCreateDescription = `After creating the pod, the pod ID is printed to stdout. + + You can then start it at any time with the podman pod start <pod_id> command. The pod will be created with the initial state 'created'.` _podCreateCommand = &cobra.Command{ Use: "create", @@ -37,6 +36,7 @@ var ( func init() { podCreateCommand.Command = _podCreateCommand + podCreateCommand.SetHelpTemplate(HelpTemplate()) podCreateCommand.SetUsageTemplate(UsageTemplate()) flags := podCreateCommand.Flags() flags.SetInterspersed(false) diff --git a/cmd/podman/pod_inspect.go b/cmd/podman/pod_inspect.go index 8b2747af0..79ffe2e6f 100644 --- a/cmd/podman/pod_inspect.go +++ b/cmd/podman/pod_inspect.go @@ -12,8 +12,11 @@ import ( var ( podInspectCommand cliconfig.PodInspectValues - podInspectDescription = "Display the configuration for a pod by name or id" - _podInspectCommand = &cobra.Command{ + podInspectDescription = `Display the configuration for a pod by name or id + + By default, this will render all results in a JSON array. If the container and image have the same name, this command returns the container JSON.` + + _podInspectCommand = &cobra.Command{ Use: "inspect [flags] POD", Short: "Displays a pod configuration", Long: podInspectDescription, @@ -28,6 +31,7 @@ var ( func init() { podInspectCommand.Command = _podInspectCommand + podInspectCommand.SetHelpTemplate(HelpTemplate()) podInspectCommand.SetUsageTemplate(UsageTemplate()) flags := podInspectCommand.Flags() flags.BoolVarP(&podInspectCommand.Latest, "latest", "l", false, "Act on the latest container podman is aware of") diff --git a/cmd/podman/pod_kill.go b/cmd/podman/pod_kill.go index 70d86d186..ebd7db762 100644 --- a/cmd/podman/pod_kill.go +++ b/cmd/podman/pod_kill.go @@ -14,8 +14,10 @@ import ( var ( podKillCommand cliconfig.PodKillValues - podKillDescription = "The main process of each container inside the specified pod will be sent SIGKILL, or any signal specified with option --signal." - _podKillCommand = &cobra.Command{ + podKillDescription = `Signals are sent to the main process of each container inside the specified pod. + + The default signal is SIGKILL, or any signal specified with option --signal.` + _podKillCommand = &cobra.Command{ Use: "kill [flags] POD [POD...]", Short: "Send the specified signal or SIGKILL to containers in pod", Long: podKillDescription, @@ -35,6 +37,7 @@ var ( func init() { podKillCommand.Command = _podKillCommand + podKillCommand.SetHelpTemplate(HelpTemplate()) podKillCommand.SetUsageTemplate(UsageTemplate()) flags := podKillCommand.Flags() flags.BoolVarP(&podKillCommand.All, "all", "a", false, "Kill all containers in all pods") diff --git a/cmd/podman/pod_pause.go b/cmd/podman/pod_pause.go index f7c90dbbe..ff29e0e1d 100644 --- a/cmd/podman/pod_pause.go +++ b/cmd/podman/pod_pause.go @@ -11,8 +11,10 @@ import ( var ( podPauseCommand cliconfig.PodPauseValues - podPauseDescription = `Pauses one or more pods. The pod name or ID can be used.` - _podPauseCommand = &cobra.Command{ + podPauseDescription = `The pod name or ID can be used. + + All running containers within each specified pod will then be paused.` + _podPauseCommand = &cobra.Command{ Use: "pause [flags] POD [POD...]", Short: "Pause one or more pods", Long: podPauseDescription, @@ -32,6 +34,7 @@ var ( func init() { podPauseCommand.Command = _podPauseCommand + podPauseCommand.SetHelpTemplate(HelpTemplate()) podPauseCommand.SetUsageTemplate(UsageTemplate()) flags := podPauseCommand.Flags() flags.BoolVarP(&podPauseCommand.All, "all", "a", false, "Pause all running pods") diff --git a/cmd/podman/pod_ps.go b/cmd/podman/pod_ps.go index 8e48740e6..e30a03005 100644 --- a/cmd/podman/pod_ps.go +++ b/cmd/podman/pod_ps.go @@ -134,6 +134,7 @@ var ( func init() { podPsCommand.Command = _podPsCommand + podPsCommand.SetHelpTemplate(HelpTemplate()) podPsCommand.SetUsageTemplate(UsageTemplate()) flags := podPsCommand.Flags() flags.BoolVar(&podPsCommand.CtrNames, "ctr-names", false, "Display the container names") diff --git a/cmd/podman/pod_restart.go b/cmd/podman/pod_restart.go index ba77e1409..0765b98db 100644 --- a/cmd/podman/pod_restart.go +++ b/cmd/podman/pod_restart.go @@ -12,8 +12,10 @@ import ( var ( podRestartCommand cliconfig.PodRestartValues - podRestartDescription = `Restarts one or more pods. The pod ID or name can be used.` - _podRestartCommand = &cobra.Command{ + podRestartDescription = `The pod ID or name can be used. + + All of the containers within each of the specified pods will be restarted. If a container in a pod is not currently running it will be started.` + _podRestartCommand = &cobra.Command{ Use: "restart [flags] POD [POD...]", Short: "Restart one or more pods", Long: podRestartDescription, @@ -33,6 +35,7 @@ var ( func init() { podRestartCommand.Command = _podRestartCommand + podRestartCommand.SetHelpTemplate(HelpTemplate()) podRestartCommand.SetUsageTemplate(UsageTemplate()) flags := podRestartCommand.Flags() flags.BoolVarP(&podRestartCommand.All, "all", "a", false, "Restart all running pods") diff --git a/cmd/podman/pod_rm.go b/cmd/podman/pod_rm.go index fa452b061..a40992818 100644 --- a/cmd/podman/pod_rm.go +++ b/cmd/podman/pod_rm.go @@ -12,11 +12,9 @@ import ( var ( podRmCommand cliconfig.PodRmValues - podRmDescription = fmt.Sprintf(` -podman rm will remove one or more pods from the host. The pod name or ID can -be used. A pod with containers will not be removed without --force. -If --force is specified, all containers will be stopped, then removed. -`) + podRmDescription = fmt.Sprintf(`podman rm will remove one or more pods from the host. + + The pod name or ID can be used. A pod with containers will not be removed without --force. If --force is specified, all containers will be stopped, then removed.`) _podRmCommand = &cobra.Command{ Use: "rm [flags] POD [POD...]", Short: "Remove one or more pods", @@ -37,6 +35,7 @@ If --force is specified, all containers will be stopped, then removed. func init() { podRmCommand.Command = _podRmCommand + podRmCommand.SetHelpTemplate(HelpTemplate()) podRmCommand.SetUsageTemplate(UsageTemplate()) flags := podRmCommand.Flags() flags.BoolVarP(&podRmCommand.All, "all", "a", false, "Remove all running pods") diff --git a/cmd/podman/pod_start.go b/cmd/podman/pod_start.go index ca8ad08cf..949af80d8 100644 --- a/cmd/podman/pod_start.go +++ b/cmd/podman/pod_start.go @@ -12,11 +12,9 @@ import ( var ( podStartCommand cliconfig.PodStartValues - podStartDescription = ` - podman pod start + podStartDescription = `The pod name or ID can be used. - Starts one or more pods. The pod name or ID can be used. -` + All containers defined in the pod will be started.` _podStartCommand = &cobra.Command{ Use: "start [flags] POD [POD...]", Short: "Start one or more pods", @@ -37,6 +35,7 @@ var ( func init() { podStartCommand.Command = _podStartCommand + podStartCommand.SetHelpTemplate(HelpTemplate()) podStartCommand.SetUsageTemplate(UsageTemplate()) flags := podStartCommand.Flags() flags.BoolVarP(&podStartCommand.All, "all", "a", false, "Start all pods") diff --git a/cmd/podman/pod_stats.go b/cmd/podman/pod_stats.go index f5edd21f8..7dbd84525 100644 --- a/cmd/podman/pod_stats.go +++ b/cmd/podman/pod_stats.go @@ -13,8 +13,8 @@ import ( tm "github.com/buger/goterm" "github.com/containers/libpod/cmd/podman/cliconfig" "github.com/containers/libpod/cmd/podman/formats" - "github.com/containers/libpod/cmd/podman/libpodruntime" "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/adapter" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/ulule/deepcopier" @@ -22,10 +22,11 @@ import ( var ( podStatsCommand cliconfig.PodStatsValues - podStatsDescription = "Display a live stream of resource usage statistics for the containers in or more pods" - _podStatsCommand = &cobra.Command{ + podStatsDescription = `For each specified pod this command will display percentage of CPU, memory, network I/O, block I/O and PIDs for containers in one the pods.` + + _podStatsCommand = &cobra.Command{ Use: "stats [flags] POD [POD...]", - Short: "Display percentage of CPU, memory, network I/O, block I/O and PIDs for containers in one or more pods", + Short: "Display a live stream of resource usage statistics for the containers in one or more pods", Long: podStatsDescription, RunE: func(cmd *cobra.Command, args []string) error { podStatsCommand.InputArgs = args @@ -40,6 +41,7 @@ var ( func init() { podStatsCommand.Command = _podStatsCommand + podStatsCommand.SetHelpTemplate(HelpTemplate()) podStatsCommand.SetUsageTemplate(UsageTemplate()) flags := podStatsCommand.Flags() flags.BoolVarP(&podStatsCommand.All, "all", "a", false, "Provide stats for all running pods") @@ -51,10 +53,6 @@ func init() { } func podStatsCmd(c *cliconfig.PodStatsValues) error { - var ( - podFunc func() ([]*libpod.Pod, error) - ) - format := c.Format all := c.All latest := c.Latest @@ -76,7 +74,7 @@ func podStatsCmd(c *cliconfig.PodStatsValues) error { all = true } - runtime, err := libpodruntime.GetRuntime(&c.PodmanCommand) + runtime, err := adapter.GetRuntime(&c.PodmanCommand) if err != nil { return errors.Wrapf(err, "could not get runtime") } @@ -87,29 +85,12 @@ func podStatsCmd(c *cliconfig.PodStatsValues) error { times = 1 } - if len(c.InputArgs) > 0 { - podFunc = func() ([]*libpod.Pod, error) { return getPodsByList(c.InputArgs, runtime) } - } else if latest { - podFunc = func() ([]*libpod.Pod, error) { - latestPod, err := runtime.GetLatestPod() - if err != nil { - return nil, err - } - return []*libpod.Pod{latestPod}, err - } - } else if all { - podFunc = runtime.GetAllPods - } else { - podFunc = runtime.GetRunningPods - } - - pods, err := podFunc() + pods, err := runtime.GetStatPods(c) if err != nil { return errors.Wrapf(err, "unable to get a list of pods") } - // First we need to get an initial pass of pod/ctr stats (these are not printed) - var podStats []*libpod.PodContainerStats + var podStats []*adapter.PodContainerStats for _, p := range pods { cons, err := p.AllContainersByID() if err != nil { @@ -120,7 +101,7 @@ func podStatsCmd(c *cliconfig.PodStatsValues) error { for _, c := range cons { emptyStats[c] = &libpod.ContainerStats{} } - ps := libpod.PodContainerStats{ + ps := adapter.PodContainerStats{ Pod: p, ContainerStats: emptyStats, } @@ -128,10 +109,10 @@ func podStatsCmd(c *cliconfig.PodStatsValues) error { } // Create empty container stat results for our first pass - var previousPodStats []*libpod.PodContainerStats + var previousPodStats []*adapter.PodContainerStats for _, p := range pods { cs := make(map[string]*libpod.ContainerStats) - pcs := libpod.PodContainerStats{ + pcs := adapter.PodContainerStats{ Pod: p, ContainerStats: cs, } @@ -164,7 +145,7 @@ func podStatsCmd(c *cliconfig.PodStatsValues) error { } for i := 0; i < times; i += step { - var newStats []*libpod.PodContainerStats + var newStats []*adapter.PodContainerStats for _, p := range pods { prevStat := getPreviousPodContainerStats(p.ID(), previousPodStats) newPodStats, err := p.GetPodStats(prevStat) @@ -174,7 +155,7 @@ func podStatsCmd(c *cliconfig.PodStatsValues) error { if err != nil { return err } - newPod := libpod.PodContainerStats{ + newPod := adapter.PodContainerStats{ Pod: p, ContainerStats: newPodStats, } @@ -202,7 +183,7 @@ func podStatsCmd(c *cliconfig.PodStatsValues) error { time.Sleep(time.Second) previousPodStats := new([]*libpod.PodContainerStats) deepcopier.Copy(newStats).To(previousPodStats) - pods, err = podFunc() + pods, err = runtime.GetStatPods(c) if err != nil { return err } @@ -211,7 +192,7 @@ func podStatsCmd(c *cliconfig.PodStatsValues) error { return nil } -func podContainerStatsToPodStatOut(stats []*libpod.PodContainerStats) []*podStatOut { +func podContainerStatsToPodStatOut(stats []*adapter.PodContainerStats) []*podStatOut { var out []*podStatOut for _, p := range stats { for _, c := range p.ContainerStats { @@ -295,7 +276,7 @@ func outputToStdOut(stats []*podStatOut) { w.Flush() } -func getPreviousPodContainerStats(podID string, prev []*libpod.PodContainerStats) map[string]*libpod.ContainerStats { +func getPreviousPodContainerStats(podID string, prev []*adapter.PodContainerStats) map[string]*libpod.ContainerStats { for _, p := range prev { if podID == p.Pod.ID() { return p.ContainerStats @@ -304,7 +285,7 @@ func getPreviousPodContainerStats(podID string, prev []*libpod.PodContainerStats return map[string]*libpod.ContainerStats{} } -func outputJson(stats []*libpod.PodContainerStats) error { +func outputJson(stats []*adapter.PodContainerStats) error { b, err := json.MarshalIndent(&stats, "", " ") if err != nil { return err diff --git a/cmd/podman/pod_stop.go b/cmd/podman/pod_stop.go index 951cf082a..f1b0ac51f 100644 --- a/cmd/podman/pod_stop.go +++ b/cmd/podman/pod_stop.go @@ -12,11 +12,9 @@ import ( var ( podStopCommand cliconfig.PodStopValues - podStopDescription = ` - podman pod stop + podStopDescription = `The pod name or ID can be used. - Stops one or more running pods. The pod name or ID can be used. -` + This command will stop all running containers in each of the specified pods.` _podStopCommand = &cobra.Command{ Use: "stop [flags] POD [POD...]", @@ -38,6 +36,7 @@ var ( func init() { podStopCommand.Command = _podStopCommand + podStopCommand.SetHelpTemplate(HelpTemplate()) podStopCommand.SetUsageTemplate(UsageTemplate()) flags := podStopCommand.Flags() flags.BoolVarP(&podStopCommand.All, "all", "a", false, "Stop all running pods") diff --git a/cmd/podman/pod_top.go b/cmd/podman/pod_top.go index 6a26e3dff..c9a6d8822 100644 --- a/cmd/podman/pod_top.go +++ b/cmd/podman/pod_top.go @@ -2,13 +2,12 @@ package main import ( "fmt" + "github.com/containers/libpod/pkg/adapter" "os" "strings" "text/tabwriter" "github.com/containers/libpod/cmd/podman/cliconfig" - "github.com/containers/libpod/cmd/podman/libpodruntime" - "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -17,12 +16,10 @@ import ( var ( podTopCommand cliconfig.PodTopValues - podTopDescription = fmt.Sprintf(`Display the running processes containers in a pod. Specify format descriptors -to alter the output. You may run "podman pod top -l pid pcpu seccomp" to print -the process ID, the CPU percentage and the seccomp mode of each process of -the latest pod. -%s -`, getDescriptorString()) + podTopDescription = fmt.Sprintf(`Specify format descriptors to alter the output. + + You may run "podman pod top -l pid pcpu seccomp" to print the process ID, the CPU percentage and the seccomp mode of each process of the latest pod. +%s`, getDescriptorString()) _podTopCommand = &cobra.Command{ Use: "top [flags] CONTAINER [FORMAT-DESCRIPTORS]", @@ -41,6 +38,7 @@ the latest pod. func init() { podTopCommand.Command = _podTopCommand + podTopCommand.SetHelpTemplate(HelpTemplate()) podTopCommand.SetUsageTemplate(UsageTemplate()) flags := podTopCommand.Flags() flags.BoolVarP(&podTopCommand.Latest, "latest,", "l", false, "Act on the latest pod podman is aware of") @@ -50,8 +48,9 @@ func init() { } func podTopCmd(c *cliconfig.PodTopValues) error { - var pod *libpod.Pod - var err error + var ( + descriptors []string + ) args := c.InputArgs if c.ListDescriptors { @@ -67,39 +66,22 @@ func podTopCmd(c *cliconfig.PodTopValues) error { return errors.Errorf("you must provide the name or id of a running pod") } - runtime, err := libpodruntime.GetRuntime(&c.PodmanCommand) + runtime, err := adapter.GetRuntime(&c.PodmanCommand) if err != nil { return errors.Wrapf(err, "error creating libpod runtime") } defer runtime.Shutdown(false) - var descriptors []string if c.Latest { descriptors = args - pod, err = runtime.GetLatestPod() } else { descriptors = args[1:] - pod, err = runtime.LookupPod(args[0]) - } - - if err != nil { - return errors.Wrapf(err, "unable to lookup requested container") } - - podStatus, err := shared.GetPodStatus(pod) - if err != nil { - return err - } - if podStatus != "Running" { - return errors.Errorf("pod top can only be used on pods with at least one running container") - } - - psOutput, err := pod.GetPodPidInformation(descriptors) + w := tabwriter.NewWriter(os.Stdout, 5, 1, 3, ' ', 0) + psOutput, err := runtime.PodTop(c, descriptors) if err != nil { return err } - - w := tabwriter.NewWriter(os.Stdout, 5, 1, 3, ' ', 0) for _, proc := range psOutput { fmt.Fprintln(w, proc) } diff --git a/cmd/podman/pod_unpause.go b/cmd/podman/pod_unpause.go index 6b142d573..0623c6abb 100644 --- a/cmd/podman/pod_unpause.go +++ b/cmd/podman/pod_unpause.go @@ -12,8 +12,10 @@ import ( var ( podUnpauseCommand cliconfig.PodUnpauseValues - podUnpauseDescription = `Unpauses one or more pods. The pod name or ID can be used.` - _podUnpauseCommand = &cobra.Command{ + podUnpauseDescription = `The podman unpause command will unpause all "paused" containers assigned to the pod. + + The pod name or ID can be used.` + _podUnpauseCommand = &cobra.Command{ Use: "unpause [flags] POD [POD...]", Short: "Unpause one or more pods", Long: podUnpauseDescription, @@ -33,6 +35,7 @@ var ( func init() { podUnpauseCommand.Command = _podUnpauseCommand + podUnpauseCommand.SetHelpTemplate(HelpTemplate()) podUnpauseCommand.SetUsageTemplate(UsageTemplate()) flags := podUnpauseCommand.Flags() flags.BoolVarP(&podUnpauseCommand.All, "all", "a", false, "Unpause all running pods") diff --git a/cmd/podman/port.go b/cmd/podman/port.go index ffb5749fb..b5a4d3eec 100644 --- a/cmd/podman/port.go +++ b/cmd/podman/port.go @@ -14,10 +14,7 @@ import ( var ( portCommand cliconfig.PortValues - portDescription = ` - podman port - - List port mappings for the CONTAINER, or lookup the public-facing port that is NAT-ed to the PRIVATE_PORT + portDescription = `List port mappings for the CONTAINER, or lookup the public-facing port that is NAT-ed to the PRIVATE_PORT ` _portCommand = &cobra.Command{ Use: "port [flags] CONTAINER", @@ -39,6 +36,7 @@ var ( func init() { portCommand.Command = _portCommand + portCommand.SetHelpTemplate(HelpTemplate()) portCommand.SetUsageTemplate(UsageTemplate()) flags := portCommand.Flags() diff --git a/cmd/podman/ps.go b/cmd/podman/ps.go index 0dedd850d..6caac2406 100644 --- a/cmd/podman/ps.go +++ b/cmd/podman/ps.go @@ -174,6 +174,7 @@ var ( ) func psInit(command *cliconfig.PsValues) { + command.SetHelpTemplate(HelpTemplate()) command.SetUsageTemplate(UsageTemplate()) flags := command.Flags() flags.BoolVarP(&command.All, "all", "a", false, "Show all the containers, default is only running containers") @@ -418,7 +419,7 @@ func generateContainerFilterFuncs(filter, filterValue string, runtime *libpod.Ru return false }, nil case "status": - if !util.StringInSlice(filterValue, []string{"created", "restarting", "running", "paused", "exited", "unknown"}) { + if !util.StringInSlice(filterValue, []string{"created", "running", "paused", "exited", "unknown"}) { return nil, errors.Errorf("%s is not a valid status", filterValue) } return func(c *libpod.Container) bool { @@ -429,6 +430,8 @@ func generateContainerFilterFuncs(filter, filterValue string, runtime *libpod.Ru state := status.String() if status == libpod.ContainerStateConfigured { state = "created" + } else if status == libpod.ContainerStateStopped { + state = "exited" } return state == filterValue }, nil diff --git a/cmd/podman/pull.go b/cmd/podman/pull.go index 5f4658fe1..7986d5530 100644 --- a/cmd/podman/pull.go +++ b/cmd/podman/pull.go @@ -23,11 +23,9 @@ import ( var ( pullCommand cliconfig.PullValues - pullDescription = ` -Pulls an image from a registry and stores it locally. -An image can be pulled using its tag or digest. If a tag is not -specified, the image with the 'latest' tag (if it exists) is pulled -` + pullDescription = `Pulls an image from a registry and stores it locally. + + An image can be pulled using its tag or digest. If a tag is not specified, the image with the 'latest' tag (if it exists) is pulled.` _pullCommand = &cobra.Command{ Use: "pull [flags] IMAGE-PATH", Short: "Pull an image from a registry", @@ -45,6 +43,7 @@ specified, the image with the 'latest' tag (if it exists) is pulled func init() { pullCommand.Command = _pullCommand + pullCommand.SetHelpTemplate(HelpTemplate()) pullCommand.SetUsageTemplate(UsageTemplate()) flags := pullCommand.Flags() flags.BoolVar(&pullCommand.AllTags, "all-tags", false, "All tagged images inthe repository will be pulled") diff --git a/cmd/podman/push.go b/cmd/podman/push.go index bc909cb5e..afc385527 100644 --- a/cmd/podman/push.go +++ b/cmd/podman/push.go @@ -20,10 +20,9 @@ import ( var ( pushCommand cliconfig.PushValues - pushDescription = fmt.Sprintf(` - Pushes an image to a specified location. - The Image "DESTINATION" uses a "transport":"details" format. - See podman-push(1) section "DESTINATION" for the expected format`) + pushDescription = fmt.Sprintf(`Pushes an image to a specified location. + + The Image "DESTINATION" uses a "transport":"details" format. See podman-push(1) section "DESTINATION" for the expected format.`) _pushCommand = &cobra.Command{ Use: "push [flags] IMAGE REGISTRY", @@ -42,6 +41,7 @@ var ( func init() { pushCommand.Command = _pushCommand + pushCommand.SetHelpTemplate(HelpTemplate()) pushCommand.SetUsageTemplate(UsageTemplate()) flags := pushCommand.Flags() flags.MarkHidden("signature-policy") diff --git a/cmd/podman/refresh.go b/cmd/podman/refresh.go index 1e4a31a52..ed2e173ab 100644 --- a/cmd/podman/refresh.go +++ b/cmd/podman/refresh.go @@ -12,8 +12,11 @@ import ( var ( refreshCommand cliconfig.RefreshValues - refreshDescription = "The refresh command resets the state of all containers to handle database changes after a Podman upgrade. All running containers will be restarted." - _refreshCommand = &cobra.Command{ + refreshDescription = `Resets the state of all containers to handle database changes after a Podman upgrade. + + All running containers will be restarted. +` + _refreshCommand = &cobra.Command{ Use: "refresh", Args: noSubArgs, Short: "Refresh container state", @@ -29,6 +32,7 @@ var ( func init() { _refreshCommand.Hidden = true refreshCommand.Command = _refreshCommand + refreshCommand.SetHelpTemplate(HelpTemplate()) refreshCommand.SetUsageTemplate(UsageTemplate()) } diff --git a/cmd/podman/restart.go b/cmd/podman/restart.go index 5aa12070e..341cbf978 100644 --- a/cmd/podman/restart.go +++ b/cmd/podman/restart.go @@ -16,8 +16,10 @@ import ( var ( restartCommand cliconfig.RestartValues - restartDescription = `Restarts one or more running containers. The container ID or name can be used. A timeout before forcibly stopping can be set, but defaults to 10 seconds` - _restartCommand = &cobra.Command{ + restartDescription = `Restarts one or more running containers. The container ID or name can be used. + + A timeout before forcibly stopping can be set, but defaults to 10 seconds.` + _restartCommand = &cobra.Command{ Use: "restart [flags] CONTAINER [CONTAINER...]", Short: "Restart one or more containers", Long: restartDescription, @@ -37,6 +39,7 @@ var ( func init() { restartCommand.Command = _restartCommand + restartCommand.SetHelpTemplate(HelpTemplate()) restartCommand.SetUsageTemplate(UsageTemplate()) flags := restartCommand.Flags() flags.BoolVarP(&restartCommand.All, "all", "a", false, "Restart all non-running containers") diff --git a/cmd/podman/restore.go b/cmd/podman/restore.go index 73d355734..0f6828432 100644 --- a/cmd/podman/restore.go +++ b/cmd/podman/restore.go @@ -40,6 +40,7 @@ var ( func init() { restoreCommand.Command = _restoreCommand + restoreCommand.SetHelpTemplate(HelpTemplate()) restoreCommand.SetUsageTemplate(UsageTemplate()) flags := restoreCommand.Flags() flags.BoolVarP(&restoreCommand.All, "all", "a", false, "Restore all checkpointed containers") diff --git a/cmd/podman/rm.go b/cmd/podman/rm.go index d23f8228c..4230bb396 100644 --- a/cmd/podman/rm.go +++ b/cmd/podman/rm.go @@ -15,11 +15,9 @@ import ( var ( rmCommand cliconfig.RmValues - rmDescription = fmt.Sprintf(` -Podman rm will remove one or more containers from the host. -The container name or ID can be used. This does not remove images. -Running containers will not be removed without the -f option. -`) + rmDescription = fmt.Sprintf(`Removes one or more containers from the host. The container name or ID can be used. + + Command does not remove images. Running containers will not be removed without the -f option.`) _rmCommand = &cobra.Command{ Use: "rm [flags] CONTAINER [CONTAINER...]", Short: "Remove one or more containers", @@ -40,6 +38,7 @@ Running containers will not be removed without the -f option. func init() { rmCommand.Command = _rmCommand + rmCommand.SetHelpTemplate(HelpTemplate()) rmCommand.SetUsageTemplate(UsageTemplate()) flags := rmCommand.Flags() flags.BoolVarP(&rmCommand.All, "all", "a", false, "Remove all containers") diff --git a/cmd/podman/rmi.go b/cmd/podman/rmi.go index 511668df7..149cd8d82 100644 --- a/cmd/podman/rmi.go +++ b/cmd/podman/rmi.go @@ -13,7 +13,7 @@ import ( var ( rmiCommand cliconfig.RmiValues - rmiDescription = "Removes one or more locally stored images." + rmiDescription = "Removes one or more previously pulled or locally created images." _rmiCommand = cobra.Command{ Use: "rmi [flags] IMAGE [IMAGE...]", Short: "Removes one or more images from local storage", @@ -30,6 +30,7 @@ var ( ) func rmiInit(command *cliconfig.RmiValues) { + command.SetHelpTemplate(HelpTemplate()) command.SetUsageTemplate(UsageTemplate()) flags := command.Flags() flags.BoolVarP(&command.All, "all", "a", false, "Remove all images") diff --git a/cmd/podman/run.go b/cmd/podman/run.go index f66b939d3..ff09e670d 100644 --- a/cmd/podman/run.go +++ b/cmd/podman/run.go @@ -39,6 +39,7 @@ var ( func init() { runCommand.Command = _runCommand + runCommand.SetHelpTemplate(HelpTemplate()) runCommand.SetUsageTemplate(UsageTemplate()) flags := runCommand.Flags() flags.SetInterspersed(false) diff --git a/cmd/podman/runlabel.go b/cmd/podman/runlabel.go index f91ffed0d..68621e095 100644 --- a/cmd/podman/runlabel.go +++ b/cmd/podman/runlabel.go @@ -38,6 +38,7 @@ Executes a command as described by a container image label. func init() { runlabelCommand.Command = _runlabelCommand + runlabelCommand.SetHelpTemplate(HelpTemplate()) runlabelCommand.SetUsageTemplate(UsageTemplate()) flags := runlabelCommand.Flags() flags.StringVar(&runlabelCommand.Authfile, "authfile", "", "Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json. Use REGISTRY_AUTH_FILE environment variable to override") diff --git a/cmd/podman/save.go b/cmd/podman/save.go index 3bc283772..494496a3d 100644 --- a/cmd/podman/save.go +++ b/cmd/podman/save.go @@ -23,9 +23,7 @@ var validFormats = []string{ociManifestDir, ociArchive, v2s2ManifestDir, v2s2Arc var ( saveCommand cliconfig.SaveValues - saveDescription = ` - Save an image to docker-archive or oci-archive on the local machine. - Default is docker-archive` + saveDescription = `Save an image to docker-archive or oci-archive on the local machine. Default is docker-archive.` _saveCommand = &cobra.Command{ Use: "save [flags] IMAGE", @@ -54,6 +52,7 @@ var ( func init() { saveCommand.Command = _saveCommand + saveCommand.SetHelpTemplate(HelpTemplate()) saveCommand.SetUsageTemplate(UsageTemplate()) flags := saveCommand.Flags() flags.BoolVar(&saveCommand.Compress, "compress", false, "Compress tarball image layers when saving to a directory using the 'dir' transport. (default is same compression type as source)") diff --git a/cmd/podman/search.go b/cmd/podman/search.go index 5c14f1ff1..e508c2bcf 100644 --- a/cmd/podman/search.go +++ b/cmd/podman/search.go @@ -18,9 +18,9 @@ const ( var ( searchCommand cliconfig.SearchValues - searchDescription = ` - Search registries for a given image. Can search all the default registries or a specific registry. - Can limit the number of results, and filter the output based on certain conditions.` + searchDescription = `Search registries for a given image. Can search all the default registries or a specific registry. + + Users can limit the number of results, and filter the output based on certain conditions.` _searchCommand = &cobra.Command{ Use: "search [flags] TERM", Short: "Search registry for image", @@ -38,6 +38,7 @@ var ( func init() { searchCommand.Command = _searchCommand + searchCommand.SetHelpTemplate(HelpTemplate()) searchCommand.SetUsageTemplate(UsageTemplate()) flags := searchCommand.Flags() flags.StringVar(&searchCommand.Authfile, "authfile", "", "Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json. Use REGISTRY_AUTH_FILE environment variable to override") diff --git a/cmd/podman/sign.go b/cmd/podman/sign.go index 2cf228d01..06418e4a5 100644 --- a/cmd/podman/sign.go +++ b/cmd/podman/sign.go @@ -22,7 +22,7 @@ import ( var ( signCommand cliconfig.SignValues - signDescription = "Create a signature file that can be used later to verify the image" + signDescription = "Create a signature file that can be used later to verify the image." _signCommand = &cobra.Command{ Use: "sign [flags] IMAGE [IMAGE...]", Short: "Sign an image", @@ -39,6 +39,7 @@ var ( func init() { signCommand.Command = _signCommand + signCommand.SetHelpTemplate(HelpTemplate()) signCommand.SetUsageTemplate(UsageTemplate()) flags := signCommand.Flags() flags.StringVarP(&signCommand.Directory, "directory", "d", "", "Define an alternate directory to store signatures") diff --git a/cmd/podman/start.go b/cmd/podman/start.go index 3ce04ea79..e942c1ccd 100644 --- a/cmd/podman/start.go +++ b/cmd/podman/start.go @@ -15,11 +15,8 @@ import ( var ( startCommand cliconfig.StartValues - startDescription = ` - podman start + startDescription = `Starts one or more containers. The container name or ID can be used.` - Starts one or more containers. The container name or ID can be used. -` _startCommand = &cobra.Command{ Use: "start [flags] CONTAINER [CONTAINER...]", Short: "Start one or more containers", @@ -37,6 +34,7 @@ var ( func init() { startCommand.Command = _startCommand + startCommand.SetHelpTemplate(HelpTemplate()) startCommand.SetUsageTemplate(UsageTemplate()) flags := startCommand.Flags() flags.BoolVarP(&startCommand.Attach, "attach", "a", false, "Attach container's STDOUT and STDERR") diff --git a/cmd/podman/stats.go b/cmd/podman/stats.go index dcb274471..3e2e114a9 100644 --- a/cmd/podman/stats.go +++ b/cmd/podman/stats.go @@ -31,10 +31,10 @@ type statsOutputParams struct { var ( statsCommand cliconfig.StatsValues - statsDescription = "display a live stream of one or more containers' resource usage statistics" + statsDescription = "Display percentage of CPU, memory, network I/O, block I/O and PIDs for one or more containers." _statsCommand = &cobra.Command{ Use: "stats [flags] CONTAINER [CONTAINER...]", - Short: "Display percentage of CPU, memory, network I/O, block I/O and PIDs for one or more containers", + Short: "Display a live stream of container resource usage statistics", Long: statsDescription, RunE: func(cmd *cobra.Command, args []string) error { statsCommand.InputArgs = args @@ -52,6 +52,7 @@ var ( func init() { statsCommand.Command = _statsCommand + statsCommand.SetHelpTemplate(HelpTemplate()) statsCommand.SetUsageTemplate(UsageTemplate()) flags := statsCommand.Flags() flags.BoolVarP(&statsCommand.All, "all", "a", false, "Show all containers. Only running containers are shown by default. The default is false") diff --git a/cmd/podman/stop.go b/cmd/podman/stop.go index 7bd160494..2a1470ad0 100644 --- a/cmd/podman/stop.go +++ b/cmd/podman/stop.go @@ -15,13 +15,9 @@ import ( var ( stopCommand cliconfig.StopValues - stopDescription = ` - podman stop + stopDescription = `Stops one or more running containers. The container name or ID can be used. - Stops one or more running containers. The container name or ID can be used. - A timeout to forcibly stop the container can also be set but defaults to 10 - seconds otherwise. -` + A timeout to forcibly stop the container can also be set but defaults to 10 seconds otherwise.` _stopCommand = &cobra.Command{ Use: "stop [flags] CONTAINER [CONTAINER...]", Short: "Stop one or more containers", @@ -42,6 +38,7 @@ var ( func init() { stopCommand.Command = _stopCommand + stopCommand.SetHelpTemplate(HelpTemplate()) stopCommand.SetUsageTemplate(UsageTemplate()) flags := stopCommand.Flags() flags.BoolVarP(&stopCommand.All, "all", "a", false, "Stop all running containers") diff --git a/cmd/podman/system.go b/cmd/podman/system.go index 741b79da5..528a594de 100644 --- a/cmd/podman/system.go +++ b/cmd/podman/system.go @@ -13,6 +13,7 @@ var ( Use: "system", Short: "Manage podman", Long: systemDescription, + RunE: commandRunE(), }, } ) diff --git a/cmd/podman/system_prune.go b/cmd/podman/system_prune.go index f566387fa..624f24acb 100644 --- a/cmd/podman/system_prune.go +++ b/cmd/podman/system_prune.go @@ -36,6 +36,7 @@ var ( func init() { pruneSystemCommand.Command = _pruneSystemCommand + pruneSystemCommand.SetHelpTemplate(HelpTemplate()) pruneSystemCommand.SetUsageTemplate(UsageTemplate()) flags := pruneSystemCommand.Flags() flags.BoolVarP(&pruneSystemCommand.All, "all", "a", false, "Remove all unused data") diff --git a/cmd/podman/system_renumber.go b/cmd/podman/system_renumber.go index 31137b9f6..ed0b28a3c 100644 --- a/cmd/podman/system_renumber.go +++ b/cmd/podman/system_renumber.go @@ -31,6 +31,7 @@ var ( func init() { renumberCommand.Command = _renumberCommand + renumberCommand.SetHelpTemplate(HelpTemplate()) renumberCommand.SetUsageTemplate(UsageTemplate()) } diff --git a/cmd/podman/tag.go b/cmd/podman/tag.go index 98c6e3449..98d9a6856 100644 --- a/cmd/podman/tag.go +++ b/cmd/podman/tag.go @@ -10,7 +10,7 @@ import ( var ( tagCommand cliconfig.TagValues - tagDescription = "Adds one or more additional names to locally-stored image" + tagDescription = "Adds one or more additional names to locally-stored image." _tagCommand = &cobra.Command{ Use: "tag [flags] IMAGE TAG [TAG...]", Short: "Add an additional name to a local image", @@ -28,6 +28,7 @@ var ( func init() { tagCommand.Command = _tagCommand + tagCommand.SetHelpTemplate(HelpTemplate()) tagCommand.SetUsageTemplate(UsageTemplate()) } diff --git a/cmd/podman/top.go b/cmd/podman/top.go index d96402f1a..c2156050c 100644 --- a/cmd/podman/top.go +++ b/cmd/podman/top.go @@ -18,20 +18,20 @@ func getDescriptorString() string { descriptors, err := libpod.GetContainerPidInformationDescriptors() if err == nil { return fmt.Sprintf(` -Format Descriptors: -%s`, strings.Join(descriptors, ",")) + Format Descriptors: + %s`, strings.Join(descriptors, ",")) } return "" } var ( topCommand cliconfig.TopValues - topDescription = fmt.Sprintf(`Display the running processes of the container. Specify format descriptors -to alter the output. You may run "podman top -l pid pcpu seccomp" to print -the process ID, the CPU percentage and the seccomp mode of each process of -the latest container. -%s -`, getDescriptorString()) + topDescription = fmt.Sprintf(`Similar to system "top" command. + + Specify format descriptors to alter the output. + + Running "podman top -l pid pcpu seccomp" will print the process ID, the CPU percentage and the seccomp mode of each process of the latest container. +%s`, getDescriptorString()) _topCommand = &cobra.Command{ Use: "top [flags] CONTAINER [FORMAT-DESCRIPTIOS]", @@ -50,6 +50,7 @@ the latest container. func init() { topCommand.Command = _topCommand + topCommand.SetHelpTemplate(HelpTemplate()) topCommand.SetUsageTemplate(UsageTemplate()) flags := topCommand.Flags() flags.BoolVar(&topCommand.ListDescriptors, "list-descriptors", false, "") diff --git a/cmd/podman/trust.go b/cmd/podman/trust.go index 8b02dcdc6..0a79e1570 100644 --- a/cmd/podman/trust.go +++ b/cmd/podman/trust.go @@ -6,16 +6,21 @@ import ( ) var ( + trustDescription = `Manages which registries you trust as a source of container images based on its location. + + The location is determined by the transport and the registry host of the image. Using this container image docker://docker.io/library/busybox as an example, docker is the transport and docker.io is the registry host.` trustCommand = cliconfig.PodmanCommand{ Command: &cobra.Command{ Use: "trust", Short: "Manage container image trust policy", - Long: "podman image trust command", + Long: trustDescription, + RunE: commandRunE(), }, } ) func init() { + trustCommand.SetHelpTemplate(HelpTemplate()) trustCommand.SetUsageTemplate(UsageTemplate()) trustCommand.AddCommand(getTrustSubCommands()...) imageCommand.AddCommand(trustCommand.Command) diff --git a/cmd/podman/trust_set_show.go b/cmd/podman/trust_set_show.go index 746854249..5a70c21cc 100644 --- a/cmd/podman/trust_set_show.go +++ b/cmd/podman/trust_set_show.go @@ -50,8 +50,10 @@ var ( func init() { setTrustCommand.Command = _setTrustCommand + setTrustCommand.SetHelpTemplate(HelpTemplate()) setTrustCommand.SetUsageTemplate(UsageTemplate()) showTrustCommand.Command = _showTrustCommand + showTrustCommand.SetHelpTemplate(HelpTemplate()) showTrustCommand.SetUsageTemplate(UsageTemplate()) setFlags := setTrustCommand.Flags() setFlags.StringVar(&setTrustCommand.PolicyPath, "policypath", "", "") diff --git a/cmd/podman/umount.go b/cmd/podman/umount.go index 48c97fa31..c57d5794c 100644 --- a/cmd/podman/umount.go +++ b/cmd/podman/umount.go @@ -14,12 +14,11 @@ import ( var ( umountCommand cliconfig.UmountValues - description = ` -Container storage increments a mount counter each time a container is mounted. -When a container is unmounted, the mount counter is decremented and the -container's root filesystem is physically unmounted only when the mount -counter reaches zero indicating no other processes are using the mount. -An unmount can be forced with the --force flag. + description = `Container storage increments a mount counter each time a container is mounted. + + When a container is unmounted, the mount counter is decremented. The container's root filesystem is physically unmounted only when the mount counter reaches zero indicating no other processes are using the mount. + + An unmount can be forced with the --force flag. ` _umountCommand = &cobra.Command{ Use: "umount [flags] CONTAINER [CONTAINER...]", @@ -42,6 +41,7 @@ An unmount can be forced with the --force flag. func init() { umountCommand.Command = _umountCommand + umountCommand.SetHelpTemplate(HelpTemplate()) umountCommand.SetUsageTemplate(UsageTemplate()) flags := umountCommand.Flags() flags.BoolVarP(&umountCommand.All, "all", "a", false, "Umount all of the currently mounted containers") diff --git a/cmd/podman/unpause.go b/cmd/podman/unpause.go index 58fd19fe1..0c52a2443 100644 --- a/cmd/podman/unpause.go +++ b/cmd/podman/unpause.go @@ -15,12 +15,8 @@ import ( var ( unpauseCommand cliconfig.UnpauseValues - unpauseDescription = ` - podman unpause - - Unpauses one or more running containers. The container name or ID can be used. -` - _unpauseCommand = &cobra.Command{ + unpauseDescription = `Unpauses one or more previously paused containers. The container name or ID can be used.` + _unpauseCommand = &cobra.Command{ Use: "unpause [flags] CONTAINER [CONTAINER...]", Short: "Unpause the processes in one or more containers", Long: unpauseDescription, @@ -36,6 +32,7 @@ var ( func init() { unpauseCommand.Command = _unpauseCommand + unpauseCommand.SetHelpTemplate(HelpTemplate()) unpauseCommand.SetUsageTemplate(UsageTemplate()) flags := unpauseCommand.Flags() flags.BoolVarP(&unpauseCommand.All, "all", "a", false, "Unpause all paused containers") diff --git a/cmd/podman/varlink.go b/cmd/podman/varlink.go index 5cc79ef96..787ad01cd 100644 --- a/cmd/podman/varlink.go +++ b/cmd/podman/varlink.go @@ -18,10 +18,9 @@ import ( var ( varlinkCommand cliconfig.VarlinkValues - varlinkDescription = ` - podman varlink + varlinkDescription = `Run varlink interface. Podman varlink listens on the specified unix domain socket for incoming connects. - run varlink interface + Tools speaking varlink protocol can remotely manage pods, containers and images. ` _varlinkCommand = &cobra.Command{ Use: "varlink [flags] URI", @@ -39,6 +38,7 @@ var ( func init() { varlinkCommand.Command = _varlinkCommand + varlinkCommand.SetHelpTemplate(HelpTemplate()) varlinkCommand.SetUsageTemplate(UsageTemplate()) flags := varlinkCommand.Flags() flags.Int64VarP(&varlinkCommand.Timeout, "timeout", "t", 1000, "Time until the varlink session expires in milliseconds. Use 0 to disable the timeout") diff --git a/cmd/podman/varlink/io.podman.varlink b/cmd/podman/varlink/io.podman.varlink index 73e6c15f9..6109bd290 100644 --- a/cmd/podman/varlink/io.podman.varlink +++ b/cmd/podman/varlink/io.podman.varlink @@ -549,6 +549,10 @@ method ExportContainer(name: string, path: string) -> (tarfile: string) # ~~~ method GetContainerStats(name: string) -> (container: ContainerStats) +# GetContainerStatsWithHistory takes a previous set of container statistics and uses libpod functions +# to calculate the containers statistics based on current and previous measurements. +method GetContainerStatsWithHistory(previousStats: ContainerStats) -> (container: ContainerStats) + # This method has not be implemented yet. # method ResizeContainerTty() -> (notimplemented: NotImplemented) @@ -617,10 +621,10 @@ method UnpauseContainer(name: string) -> (container: string) # ~~~ method GetAttachSockets(name: string) -> (sockets: Sockets) -# WaitContainer takes the name or ID of a container and waits until the container stops. Upon stopping, the return -# code of the container is returned. If the container container cannot be found by ID or name, -# a [ContainerNotFound](#ContainerNotFound) error is returned. -method WaitContainer(name: string) -> (exitcode: int) +# WaitContainer takes the name or ID of a container and waits the given interval in milliseconds until the container +# stops. Upon stopping, the return code of the container is returned. If the container container cannot be found by ID +# or name, a [ContainerNotFound](#ContainerNotFound) error is returned. +method WaitContainer(name: string, interval: int) -> (exitcode: int) # RemoveContainer requires the name or ID of container as well a boolean representing whether a running container can be stopped and removed, and a boolean # indicating whether to remove builtin volumes. Upon successful removal of the @@ -952,8 +956,7 @@ method RemovePod(name: string, force: bool) -> (pod: string) # This method has not be implemented yet. # method WaitPod() -> (notimplemented: NotImplemented) -# This method has not been implemented yet. -# method TopPod() -> (notimplemented: NotImplemented) +method TopPod(pod: string, latest: bool, descriptors: []string) -> (stats: []string) # GetPodStats takes the name or ID of a pod and returns a pod name and slice of ContainerStats structure which # contains attributes like memory and cpu usage. If the pod cannot be found, a [PodNotFound](#PodNotFound) diff --git a/cmd/podman/volume.go b/cmd/podman/volume.go index 8a8664151..2a071d0c7 100644 --- a/cmd/podman/volume.go +++ b/cmd/podman/volume.go @@ -5,15 +5,14 @@ import ( "github.com/spf13/cobra" ) -var volumeDescription = `Manage volumes. - -Volumes are created in and can be shared between containers.` +var volumeDescription = `Volumes are created in and can be shared between containers.` var volumeCommand = cliconfig.PodmanCommand{ Command: &cobra.Command{ Use: "volume", Short: "Manage volumes", Long: volumeDescription, + RunE: commandRunE(), }, } var volumeSubcommands = []*cobra.Command{ diff --git a/cmd/podman/volume_create.go b/cmd/podman/volume_create.go index 96b2ed8c7..d873f9806 100644 --- a/cmd/podman/volume_create.go +++ b/cmd/podman/volume_create.go @@ -11,11 +11,7 @@ import ( var ( volumeCreateCommand cliconfig.VolumeCreateValues - volumeCreateDescription = ` -podman volume create - -Creates a new volume. If using the default driver, "local", the volume will -be created at.` + volumeCreateDescription = `If using the default driver, "local", the volume will be created on the host in the volumes directory under container storage.` _volumeCreateCommand = &cobra.Command{ Use: "create [flags] [NAME]", @@ -34,6 +30,7 @@ be created at.` func init() { volumeCreateCommand.Command = _volumeCreateCommand + volumeCommand.SetHelpTemplate(HelpTemplate()) volumeCreateCommand.SetUsageTemplate(UsageTemplate()) flags := volumeCreateCommand.Flags() flags.StringVar(&volumeCreateCommand.Driver, "driver", "", "Specify volume driver name (default local)") diff --git a/cmd/podman/volume_inspect.go b/cmd/podman/volume_inspect.go index 8add7a375..fdd8b5b0b 100644 --- a/cmd/podman/volume_inspect.go +++ b/cmd/podman/volume_inspect.go @@ -9,12 +9,9 @@ import ( var ( volumeInspectCommand cliconfig.VolumeInspectValues - volumeInspectDescription = ` -podman volume inspect + volumeInspectDescription = `Display detailed information on one or more volumes. -Display detailed information on one or more volumes. Can change the format -from JSON to a Go template. -` + Use a Go template to change the format from JSON.` _volumeInspectCommand = &cobra.Command{ Use: "inspect [flags] VOLUME [VOLUME...]", Short: "Display detailed information on one or more volumes", @@ -32,6 +29,7 @@ from JSON to a Go template. func init() { volumeInspectCommand.Command = _volumeInspectCommand + volumeInspectCommand.SetHelpTemplate(HelpTemplate()) volumeInspectCommand.SetUsageTemplate(UsageTemplate()) flags := volumeInspectCommand.Flags() flags.BoolVarP(&volumeInspectCommand.All, "all", "a", false, "Inspect all volumes") diff --git a/cmd/podman/volume_ls.go b/cmd/podman/volume_ls.go index 6855f38e0..5a36f4f7d 100644 --- a/cmd/podman/volume_ls.go +++ b/cmd/podman/volume_ls.go @@ -44,8 +44,7 @@ var ( podman volume ls List all available volumes. The output of the volumes can be filtered -and the output format can be changed to JSON or a user specified Go template. -` +and the output format can be changed to JSON or a user specified Go template.` _volumeLsCommand = &cobra.Command{ Use: "ls", Aliases: []string{"list"}, @@ -62,6 +61,7 @@ and the output format can be changed to JSON or a user specified Go template. func init() { volumeLsCommand.Command = _volumeLsCommand + volumeLsCommand.SetHelpTemplate(HelpTemplate()) volumeLsCommand.SetUsageTemplate(UsageTemplate()) flags := volumeLsCommand.Flags() diff --git a/cmd/podman/volume_prune.go b/cmd/podman/volume_prune.go index 370f072eb..70ba506e7 100644 --- a/cmd/podman/volume_prune.go +++ b/cmd/podman/volume_prune.go @@ -16,12 +16,10 @@ import ( var ( volumePruneCommand cliconfig.VolumePruneValues - volumePruneDescription = ` -podman volume prune + volumePruneDescription = `Volumes that are not currently owned by a container will be removed. -Remove all unused volumes. Will prompt for confirmation if not -using force. -` + The command prompts for confirmation which can be overridden with the --force flag. + Note all data will be destroyed.` _volumePruneCommand = &cobra.Command{ Use: "prune", Args: noSubArgs, @@ -37,6 +35,7 @@ using force. func init() { volumePruneCommand.Command = _volumePruneCommand + volumePruneCommand.SetHelpTemplate(HelpTemplate()) volumePruneCommand.SetUsageTemplate(UsageTemplate()) flags := volumePruneCommand.Flags() diff --git a/cmd/podman/volume_rm.go b/cmd/podman/volume_rm.go index 73b1a6668..8c6d5e97a 100644 --- a/cmd/podman/volume_rm.go +++ b/cmd/podman/volume_rm.go @@ -11,13 +11,9 @@ import ( var ( volumeRmCommand cliconfig.VolumeRmValues - volumeRmDescription = ` -podman volume rm + volumeRmDescription = `Remove one or more existing volumes. -Remove one or more existing volumes. Will only remove volumes that are -not being used by any containers. To remove the volumes anyways, use the ---force flag. -` + By default only volumes that are not being used by any containers will be removed. To remove the volumes anyways, use the --force flag.` _volumeRmCommand = &cobra.Command{ Use: "rm [flags] VOLUME [VOLUME...]", Aliases: []string{"remove"}, @@ -36,6 +32,7 @@ not being used by any containers. To remove the volumes anyways, use the func init() { volumeRmCommand.Command = _volumeRmCommand + volumeRmCommand.SetHelpTemplate(HelpTemplate()) volumeRmCommand.SetUsageTemplate(UsageTemplate()) flags := volumeRmCommand.Flags() flags.BoolVarP(&volumeRmCommand.All, "all", "a", false, "Remove all volumes") diff --git a/cmd/podman/wait.go b/cmd/podman/wait.go index 9df2e3208..4449898a0 100644 --- a/cmd/podman/wait.go +++ b/cmd/podman/wait.go @@ -2,11 +2,11 @@ package main import ( "fmt" - "os" + "reflect" "time" "github.com/containers/libpod/cmd/podman/cliconfig" - "github.com/containers/libpod/cmd/podman/libpodruntime" + "github.com/containers/libpod/pkg/adapter" "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -14,10 +14,7 @@ import ( var ( waitCommand cliconfig.WaitValues - waitDescription = ` - podman wait - - Block until one or more containers stop and then print their exit codes + waitDescription = `Block until one or more containers stop and then print their exit codes. ` _waitCommand = &cobra.Command{ Use: "wait [flags] CONTAINER [CONTAINER...]", @@ -36,6 +33,7 @@ var ( func init() { waitCommand.Command = _waitCommand + waitCommand.SetHelpTemplate(HelpTemplate()) waitCommand.SetUsageTemplate(UsageTemplate()) flags := waitCommand.Flags() flags.UintVarP(&waitCommand.Interval, "interval", "i", 250, "Milliseconds to wait before polling for completion") @@ -49,43 +47,36 @@ func waitCmd(c *cliconfig.WaitValues) error { return errors.Errorf("you must provide at least one container name or id") } - runtime, err := libpodruntime.GetRuntime(&c.PodmanCommand) + if c.Interval == 0 { + return errors.Errorf("interval must be greater then 0") + } + interval := time.Duration(c.Interval) * time.Millisecond + + runtime, err := adapter.GetRuntime(&c.PodmanCommand) if err != nil { - return errors.Wrapf(err, "error creating libpod runtime") + return errors.Wrapf(err, "error creating runtime") } defer runtime.Shutdown(false) + ok, failures, err := runtime.WaitOnContainers(getContext(), c, interval) if err != nil { - return errors.Wrapf(err, "could not get config") + return err } - var lastError error - if c.Latest { - latestCtr, err := runtime.GetLatestContainer() - if err != nil { - return errors.Wrapf(err, "unable to wait on latest container") - } - args = append(args, latestCtr.ID()) + for _, id := range ok { + fmt.Println(id) } - for _, container := range args { - ctr, err := runtime.LookupContainer(container) - if err != nil { - return errors.Wrapf(err, "unable to find container %s", container) - } - if c.Interval == 0 { - return errors.Errorf("interval must be greater then 0") - } - returnCode, err := ctr.WaitWithInterval(time.Duration(c.Interval) * time.Millisecond) - if err != nil { - if lastError != nil { - fmt.Fprintln(os.Stderr, lastError) - } - lastError = errors.Wrapf(err, "failed to wait for the container %v", container) - } else { - fmt.Println(returnCode) + if len(failures) > 0 { + keys := reflect.ValueOf(failures).MapKeys() + lastKey := keys[len(keys)-1].String() + lastErr := failures[lastKey] + delete(failures, lastKey) + + for _, err := range failures { + outputError(err) } + return lastErr } - - return lastError + return nil } diff --git a/completions/bash/podman b/completions/bash/podman index 74e3a49d2..a6445e14e 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 @@ -2308,6 +2328,15 @@ _podman_load() { _complete_ "$options_with_args" "$boolean_options" } +_podman_cp() { + local boolean_options=" + --help + -h + --extract + " + _complete_ "$boolean_options" +} + _podman_login() { local options_with_args=" --username @@ -2338,6 +2367,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="" @@ -2974,11 +3024,13 @@ _podman_podman() { build commit container + cp create diff exec export generate + healthcheck history image images diff --git a/contrib/cirrus/README.md b/contrib/cirrus/README.md index 0d91301c6..e3b3182ec 100644 --- a/contrib/cirrus/README.md +++ b/contrib/cirrus/README.md @@ -33,6 +33,17 @@ task (pass or fail) is set based on the exit status of the last script to execut the vendor.conf, the code and the vendored packages in ./vendor are in sync at all times. +### ``meta`` Task + +***N/B: Steps below are performed by automation*** + +1. Launch a container built from definition in ``./contrib/imgts``. + +2. Update VM Image metadata to help track usage across all automation. + +4. Always exits successfully unless there's a major problem. + + ### ``testing`` Task ***N/B: Steps below are performed by automation*** diff --git a/contrib/cirrus/test/test_dot_cirrus_yaml.py b/contrib/cirrus/test/test_dot_cirrus_yaml.py new file mode 100755 index 000000000..2894bc45e --- /dev/null +++ b/contrib/cirrus/test/test_dot_cirrus_yaml.py @@ -0,0 +1,78 @@ +#!/bin/env python3 + +import sys +import os +import os.path +import unittest +import warnings +import yaml + +class TestCaseBase(unittest.TestCase): + + SCRIPT_PATH = os.path.realpath((os.path.dirname(sys.argv[0]))) + CIRRUS_WORKING_DIR = os.environ.get('CIRRUS_WORKING_DIR', + '{0}/../../../'.format(SCRIPT_PATH)) + + def setUp(self): + os.chdir(self.CIRRUS_WORKING_DIR) + + +class TestCirrusYAML(TestCaseBase): + + IMAGE_NAME_SUFFIX = '_CACHE_IMAGE_NAME' + ACTIVE_IMAGES_NAME = 'ACTIVE_CACHE_IMAGE_NAMES' + + def setUp(self): + TestCirrusYAML._cirrus = None + super().setUp() + + @property + def cirrus(self): + if TestCirrusYAML._cirrus is None: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore",category=DeprecationWarning) + with open('.cirrus.yml', "r") as dot_cirrus_dot_yaml: + TestCirrusYAML._cirrus = yaml.load(dot_cirrus_dot_yaml) + return TestCirrusYAML._cirrus + + def _assert_get_cache_image_names(self, env): + inames = set([key for key in env.keys() + if key.endswith(self.IMAGE_NAME_SUFFIX)]) + self.assertNotEqual(inames, set()) + + ivalues = set([value for key, value in env.items() + if key in inames]) + self.assertNotEqual(ivalues, set()) + return ivalues + + def _assert_get_subdct(self, key, dct): + self.assertIn(key, dct) + return dct[key] + + def test_parse_yaml(self): + self.assertIsInstance(self.cirrus, dict) + + def test_active_cache_image_names(self): + env = self._assert_get_subdct('env', self.cirrus) + acin = self._assert_get_subdct(self.ACTIVE_IMAGES_NAME, env) + + for ivalue in self._assert_get_cache_image_names(env): + self.assertIn(ivalue, acin, + "The '{}' sub-key of 'env' should contain this among" + " its space-separated values." + "".format(self.ACTIVE_IMAGES_NAME)) + + + def test_cache_image_names_active(self): + env = self._assert_get_subdct('env', self.cirrus) + ivalues = self._assert_get_cache_image_names(env) + + for avalue in set(self._assert_get_subdct(self.ACTIVE_IMAGES_NAME, env).split()): + self.assertIn(avalue, ivalues, + "All space-separated values in the '{}' sub-key" + " of 'env' must also be used in a key with a '{}' suffix." + "".format(self.ACTIVE_IMAGES_NAME, self.IMAGE_NAME_SUFFIX)) + + +if __name__ == '__main__': + unittest.main(failfast=True, catchbreak=True, verbosity=0) diff --git a/contrib/gate/Dockerfile b/contrib/gate/Dockerfile index 4d88ae9a6..16d5eda67 100644 --- a/contrib/gate/Dockerfile +++ b/contrib/gate/Dockerfile @@ -29,6 +29,7 @@ RUN dnf -y install \ python3-dateutil \ python3-psutil \ python3-pytoml \ + python3-pyyaml \ python3-varlink \ skopeo-containers \ slirp4netns \ diff --git a/docs/podman-cp.1.md b/docs/podman-cp.1.md index 37426b236..7774542e8 100644 --- a/docs/podman-cp.1.md +++ b/docs/podman-cp.1.md @@ -54,6 +54,11 @@ You can also use : when specifying paths to a **SRC_PATH** or **DEST_PATH** on a If you use a : in a local machine path, you must be explicit with a relative or absolute path, for example: `/path/to/file:name.txt` or `./file:name.txt` +## OPTIONS + +**--extract** + +Extract the tar file into the destination directory. If the destination directory is not provided, extract the tar file into the root directory. ## ALTERNATIVES @@ -100,5 +105,7 @@ podman cp containerID:/myapp/ /myapp/ podman cp containerID:/home/myuser/. /home/myuser/ +podman cp --extract /home/myuser/myfiles.tar.gz containerID:/myfiles + ## SEE ALSO podman(1), podman-mount(1), podman-umount(1) 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/docs/podman.1.md b/docs/podman.1.md index bc03d3c5a..5c930995c 100644 --- a/docs/podman.1.md +++ b/docs/podman.1.md @@ -137,6 +137,7 @@ the exit codes follow the `chroot` standard, see below: | [podman-exec(1)](podman-exec.1.md) | Execute a command in a running container. | | [podman-export(1)](podman-export.1.md) | Export a container's filesystem contents as a tar archive. | | [podman-generate(1)](podman-generate.1.md)| Generate structured data based for a containers and pods. | +| [podman-healthcheck(1)](podman-healthcheck.1.md)| Manage healthchecks for containers | | [podman-history(1)](podman-history.1.md) | Show the history of an image. | | [podman-image(1)](podman-image.1.md) | Manage Images. | | [podman-images(1)](podman-images.1.md) | List images in local storage. | diff --git a/hack/podman-commands.sh b/hack/podman-commands.sh index 754f2923d..eb599763c 100755 --- a/hack/podman-commands.sh +++ b/hack/podman-commands.sh @@ -35,6 +35,9 @@ function podman_man() { # | [podman-cmd(1)\[(podman-cmd.1.md) | Description ... | # For all such, print the 'cmd' portion (the one in brackets). sed -ne 's/^|\s\+\[podman-\([a-z]\+\)(1.*/\1/p' <docs/$1.1.md + + # Special case: there is no podman-help man page, nor need for such. + echo "help" elif [ "$@" = "podman-image-trust" ]; then # Special case: set and show aren't actually in a table in the man page echo set diff --git a/libpod/boltdb_state.go b/libpod/boltdb_state.go index c226a0617..92a7b1538 100644 --- a/libpod/boltdb_state.go +++ b/libpod/boltdb_state.go @@ -382,6 +382,11 @@ func (s *BoltState) LookupContainer(idOrName string) (*Container, error) { return err } + namesBucket, err := getNamesBucket(tx) + if err != nil { + return err + } + nsBucket, err := getNSBucket(tx) if err != nil { return err @@ -395,41 +400,59 @@ func (s *BoltState) LookupContainer(idOrName string) (*Container, error) { // It might not be in our namespace, but // getContainerFromDB() will handle that case. id = []byte(idOrName) - } else { - // They did not give us a full container ID. - // Search for partial ID or full name matches - // Use else-if in case the name is set to a partial ID - exists := false - err = idBucket.ForEach(func(checkID, checkName []byte) error { - // If the container isn't in our namespace, we - // can't match it - if s.namespaceBytes != nil { - ns := nsBucket.Get(checkID) - if !bytes.Equal(ns, s.namespaceBytes) { - return nil - } + return s.getContainerFromDB(id, ctr, ctrBucket) + } + + // Next, check if the full name was given + isPod := false + fullID := namesBucket.Get([]byte(idOrName)) + if fullID != nil { + // The name exists and maps to an ID. + // However, we are not yet certain the ID is a + // container. + ctrExists = ctrBucket.Bucket(fullID) + if ctrExists != nil { + // A container bucket matching the full ID was + // found. + return s.getContainerFromDB(fullID, ctr, ctrBucket) + } + // Don't error if we have a name match but it's not a + // container - there's a chance we have a container with + // an ID starting with those characters. + // However, so we can return a good error, note whether + // this is a pod. + isPod = true + } + + // We were not given a full container ID or name. + // Search for partial ID matches. + exists := false + err = idBucket.ForEach(func(checkID, checkName []byte) error { + // If the container isn't in our namespace, we + // can't match it + if s.namespaceBytes != nil { + ns := nsBucket.Get(checkID) + if !bytes.Equal(ns, s.namespaceBytes) { + return nil } - if string(checkName) == idOrName { - if exists { - return errors.Wrapf(ErrCtrExists, "more than one result for ID or name %s", idOrName) - } - id = checkID - exists = true - } else if strings.HasPrefix(string(checkID), idOrName) { - if exists { - return errors.Wrapf(ErrCtrExists, "more than one result for ID or name %s", idOrName) - } - id = checkID - exists = true + } + if strings.HasPrefix(string(checkID), idOrName) { + if exists { + return errors.Wrapf(ErrCtrExists, "more than one result for container ID %s", idOrName) } + id = checkID + exists = true + } - return nil - }) - if err != nil { - return err - } else if !exists { - return errors.Wrapf(ErrNoSuchCtr, "no container with name or ID %s found", idOrName) + return nil + }) + if err != nil { + return err + } else if !exists { + if isPod { + return errors.Wrapf(ErrNoSuchCtr, "%s is a pod, not a container", idOrName) } + return errors.Wrapf(ErrNoSuchCtr, "no container with name or ID %s found", idOrName) } return s.getContainerFromDB(id, ctr, ctrBucket) @@ -941,6 +964,11 @@ func (s *BoltState) LookupPod(idOrName string) (*Pod, error) { return err } + namesBkt, err := getNamesBucket(tx) + if err != nil { + return err + } + nsBkt, err := getNSBucket(tx) if err != nil { return err @@ -954,41 +982,56 @@ func (s *BoltState) LookupPod(idOrName string) (*Pod, error) { // It might not be in our namespace, but getPodFromDB() // will handle that case. id = []byte(idOrName) - } else { - // They did not give us a full pod ID. - // Search for partial ID or full name matches - // Use else-if in case the name is set to a partial ID - exists := false - err = idBucket.ForEach(func(checkID, checkName []byte) error { - // If the pod isn't in our namespace, we - // can't match it - if s.namespaceBytes != nil { - ns := nsBkt.Get(checkID) - if !bytes.Equal(ns, s.namespaceBytes) { - return nil - } + return s.getPodFromDB(id, pod, podBkt) + } + + // Next, check if the full name was given + isCtr := false + fullID := namesBkt.Get([]byte(idOrName)) + if fullID != nil { + // The name exists and maps to an ID. + // However, we aren't yet sure if the ID is a pod. + podExists = podBkt.Bucket(fullID) + if podExists != nil { + // A pod bucket matching the full ID was found. + return s.getPodFromDB(fullID, pod, podBkt) + } + // Don't error if we have a name match but it's not a + // pod - there's a chance we have a pod with an ID + // starting with those characters. + // However, so we can return a good error, note whether + // this is a container. + isCtr = true + } + // They did not give us a full pod name or ID. + // Search for partial ID matches. + exists := false + err = idBucket.ForEach(func(checkID, checkName []byte) error { + // If the pod isn't in our namespace, we + // can't match it + if s.namespaceBytes != nil { + ns := nsBkt.Get(checkID) + if !bytes.Equal(ns, s.namespaceBytes) { + return nil } - if string(checkName) == idOrName { - if exists { - return errors.Wrapf(ErrPodExists, "more than one result for ID or name %s", idOrName) - } - id = checkID - exists = true - } else if strings.HasPrefix(string(checkID), idOrName) { - if exists { - return errors.Wrapf(ErrPodExists, "more than one result for ID or name %s", idOrName) - } - id = checkID - exists = true + } + if strings.HasPrefix(string(checkID), idOrName) { + if exists { + return errors.Wrapf(ErrPodExists, "more than one result for ID or name %s", idOrName) } + id = checkID + exists = true + } - return nil - }) - if err != nil { - return err - } else if !exists { - return errors.Wrapf(ErrNoSuchPod, "no pod with name or ID %s found", idOrName) + return nil + }) + if err != nil { + return err + } else if !exists { + if isCtr { + return errors.Wrapf(ErrNoSuchPod, "%s is a container, not a pod", idOrName) } + return errors.Wrapf(ErrNoSuchPod, "no pod with name or ID %s found", idOrName) } // We might have found a container ID, but it's OK 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/container_internal.go b/libpod/container_internal.go index e3753d825..d86062e38 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -34,8 +34,8 @@ const ( ) var ( - // localeToLanguage maps from locale values to language tags. - localeToLanguage = map[string]string{ + // localeToLanguageMap maps from locale values to language tags. + localeToLanguageMap = map[string]string{ "": "und-u-va-posix", "c": "und-u-va-posix", "posix": "und-u-va-posix", @@ -1231,6 +1231,23 @@ func (c *Container) writeStringToRundir(destFile, output string) (string, error) return filepath.Join(c.state.DestinationRunDir, destFile), nil } +// appendStringToRundir appends the provided string to the runtimedir file +func (c *Container) appendStringToRundir(destFile, output string) (string, error) { + destFileName := filepath.Join(c.state.RunDir, destFile) + + f, err := os.OpenFile(destFileName, os.O_APPEND|os.O_WRONLY, 0600) + if err != nil { + return "", errors.Wrapf(err, "unable to open %s", destFileName) + } + defer f.Close() + + if _, err := f.WriteString(output); err != nil { + return "", errors.Wrapf(err, "unable to write %s", destFileName) + } + + return filepath.Join(c.state.DestinationRunDir, destFile), nil +} + // Save OCI spec to disk, replacing any existing specs for the container func (c *Container) saveSpec(spec *spec.Spec) error { // If the OCI spec already exists, we need to replace it @@ -1264,6 +1281,16 @@ func (c *Container) saveSpec(spec *spec.Spec) error { return nil } +// localeToLanguage translates POSIX locale strings to BCP 47 language tags. +func localeToLanguage(locale string) string { + locale = strings.Replace(strings.SplitN(locale, ".", 2)[0], "_", "-", 1) + langString, ok := localeToLanguageMap[strings.ToLower(locale)] + if !ok { + langString = locale + } + return langString +} + // Warning: precreate hooks may alter 'config' in place. func (c *Container) setupOCIHooks(ctx context.Context, config *spec.Spec) (extensionStageHooks map[string][]spec.Hook, err error) { var locale string @@ -1279,11 +1306,7 @@ func (c *Container) setupOCIHooks(ctx context.Context, config *spec.Spec) (exten } } - langString, ok := localeToLanguage[strings.ToLower(locale)] - if !ok { - langString = locale - } - + langString := localeToLanguage(locale) lang, err := language.Parse(langString) if err != nil { logrus.Warnf("failed to parse language %q: %s", langString, err) diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index 0e9a5124e..5f9e5a20c 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -698,13 +698,29 @@ func (c *Container) makeBindMounts() error { // If it doesn't, don't copy them resolvPath, exists := bindMounts["/etc/resolv.conf"] if exists { - c.state.BindMounts["/etc/resolv.conf"] = resolvPath } + + // check if dependency container has an /etc/hosts file hostsPath, exists := bindMounts["/etc/hosts"] - if exists { - c.state.BindMounts["/etc/hosts"] = hostsPath + if !exists { + return errors.Errorf("error finding hosts file of dependency container %s for container %s", depCtr.ID(), c.ID()) + } + + depCtr.lock.Lock() + // generate a hosts file for the dependency container, + // based on either its old hosts file, or the default, + // and add the relevant information from the new container (hosts and IP) + hostsPath, err = depCtr.appendHosts(hostsPath, c) + + if err != nil { + depCtr.lock.Unlock() + return errors.Wrapf(err, "error creating hosts file for container %s which depends on container %s", c.ID(), depCtr.ID()) } + depCtr.lock.Unlock() + + // finally, save it in the new container + c.state.BindMounts["/etc/hosts"] = hostsPath } else { newResolv, err := c.generateResolvConf() if err != nil { @@ -712,7 +728,7 @@ func (c *Container) makeBindMounts() error { } c.state.BindMounts["/etc/resolv.conf"] = newResolv - newHosts, err := c.generateHosts() + newHosts, err := c.generateHosts("/etc/hosts") if err != nil { return errors.Wrapf(err, "error creating hosts file for container %s", c.ID()) } @@ -854,12 +870,28 @@ func (c *Container) generateResolvConf() (string, error) { } // generateHosts creates a containers hosts file -func (c *Container) generateHosts() (string, error) { - orig, err := ioutil.ReadFile("/etc/hosts") +func (c *Container) generateHosts(path string) (string, error) { + orig, err := ioutil.ReadFile(path) if err != nil { - return "", errors.Wrapf(err, "unable to read /etc/hosts") + return "", errors.Wrapf(err, "unable to read %s", path) } hosts := string(orig) + hosts += c.getHosts() + return c.writeStringToRundir("hosts", hosts) +} + +// appendHosts appends a container's config and state pertaining to hosts to a container's +// local hosts file. netCtr is the container from which the netNS information is +// taken. +// path is the basis of the hosts file, into which netCtr's netNS information will be appended. +func (c *Container) appendHosts(path string, netCtr *Container) (string, error) { + return c.appendStringToRundir("hosts", netCtr.getHosts()) +} + +// getHosts finds the pertinent information for a container's host file in its config and state +// and returns a string in a format that can be written to the host file +func (c *Container) getHosts() string { + var hosts string if len(c.config.HostAdd) > 0 { for _, host := range c.config.HostAdd { // the host format has already been verified at this point @@ -871,7 +903,7 @@ func (c *Container) generateHosts() (string, error) { ipAddress := strings.Split(c.state.NetworkStatus[0].IPs[0].Address.String(), "/")[0] hosts += fmt.Sprintf("%s\t%s\n", ipAddress, c.Hostname()) } - return c.writeStringToRundir("hosts", hosts) + return hosts } // generatePasswd generates a container specific passwd file, diff --git a/libpod/container_internal_test.go b/libpod/container_internal_test.go index f1e2b70a7..1654af929 100644 --- a/libpod/container_internal_test.go +++ b/libpod/container_internal_test.go @@ -17,6 +17,54 @@ import ( // hookPath is the path to an example hook executable. var hookPath string +func TestLocaleToLanguage(t *testing.T) { + for _, testCase := range []struct { + locale string + language string + }{ + { + locale: "", + language: "und-u-va-posix", + }, + { + locale: "C", + language: "und-u-va-posix", + }, + { + locale: "POSIX", + language: "und-u-va-posix", + }, + { + locale: "c", + language: "und-u-va-posix", + }, + { + locale: "en", + language: "en", + }, + { + locale: "en_US", + language: "en-US", + }, + { + locale: "en.UTF-8", + language: "en", + }, + { + locale: "en_US.UTF-8", + language: "en-US", + }, + { + locale: "does-not-exist", + language: "does-not-exist", + }, + } { + t.Run(testCase.locale, func(t *testing.T) { + assert.Equal(t, testCase.language, localeToLanguage(testCase.locale)) + }) + } +} + func TestPostDeleteHooks(t *testing.T) { ctx := context.Background() dir, err := ioutil.TempDir("", "libpod_test_") 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/oci.go b/libpod/oci.go index 2cbf25699..c3b5f9af2 100644 --- a/libpod/oci.go +++ b/libpod/oci.go @@ -805,6 +805,12 @@ func (r *OCIRuntime) execContainer(c *Container, cmd, capAdd, env []string, tty execCmd.Env = append(execCmd.Env, fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)) + if preserveFDs > 0 { + for fd := 3; fd < 3+preserveFDs; fd++ { + execCmd.ExtraFiles = append(execCmd.ExtraFiles, os.NewFile(uintptr(fd), fmt.Sprintf("fd-%d", fd))) + } + } + if err := execCmd.Start(); err != nil { return nil, errors.Wrapf(err, "cannot start container %s", c.ID()) } 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/libpod/runtime_pod_linux.go b/libpod/runtime_pod_linux.go index c378d18e4..9063390bd 100644 --- a/libpod/runtime_pod_linux.go +++ b/libpod/runtime_pod_linux.go @@ -95,9 +95,12 @@ func (r *Runtime) NewPod(ctx context.Context, options ...PodCreateOption) (*Pod, if pod.config.UsePodCgroup { logrus.Debugf("Got pod cgroup as %s", pod.state.CgroupPath) } - if pod.HasInfraContainer() != pod.SharesNamespaces() { + if !pod.HasInfraContainer() && pod.SharesNamespaces() { return nil, errors.Errorf("Pods must have an infra container to share namespaces") } + if pod.HasInfraContainer() && !pod.SharesNamespaces() { + logrus.Warnf("Pod has an infra container, but shares no namespaces") + } if err := r.state.AddPod(pod); err != nil { return nil, errors.Wrapf(err, "error adding pod to state") diff --git a/pkg/adapter/containers.go b/pkg/adapter/containers.go index 7514f30d2..756369196 100644 --- a/pkg/adapter/containers.go +++ b/pkg/adapter/containers.go @@ -4,6 +4,9 @@ package adapter import ( "context" + "strconv" + "syscall" + "time" "github.com/containers/libpod/cmd/podman/cliconfig" "github.com/containers/libpod/libpod" @@ -79,3 +82,48 @@ func (r *LocalRuntime) StopContainers(ctx context.Context, cli *cliconfig.StopVa } return ok, failures, nil } + +// KillContainers sends signal to container(s) based on CLI inputs. +// Returns list of successful id(s), map of failed id(s) + error, or error not from container +func (r *LocalRuntime) KillContainers(ctx context.Context, cli *cliconfig.KillValues, signal syscall.Signal) ([]string, map[string]error, error) { + var ( + ok = []string{} + failures = map[string]error{} + ) + + ctrs, err := shortcuts.GetContainersByContext(cli.All, cli.Latest, cli.InputArgs, r.Runtime) + if err != nil { + return ok, failures, err + } + + for _, c := range ctrs { + if err := c.Kill(uint(signal)); err == nil { + ok = append(ok, c.ID()) + } else { + failures[c.ID()] = err + } + } + return ok, failures, nil +} + +// WaitOnContainers waits for all given container(s) to stop +func (r *LocalRuntime) WaitOnContainers(ctx context.Context, cli *cliconfig.WaitValues, interval time.Duration) ([]string, map[string]error, error) { + var ( + ok = []string{} + failures = map[string]error{} + ) + + ctrs, err := shortcuts.GetContainersByContext(false, cli.Latest, cli.InputArgs, r.Runtime) + if err != nil { + return ok, failures, err + } + + for _, c := range ctrs { + if returnCode, err := c.WaitWithInterval(interval); err == nil { + ok = append(ok, strconv.Itoa(int(returnCode))) + } else { + failures[c.ID()] = err + } + } + return ok, failures, err +} diff --git a/pkg/adapter/containers_remote.go b/pkg/adapter/containers_remote.go index df40c8efd..5646d2297 100644 --- a/pkg/adapter/containers_remote.go +++ b/pkg/adapter/containers_remote.go @@ -6,6 +6,9 @@ import ( "context" "encoding/json" "errors" + "strconv" + "syscall" + "time" "github.com/containers/libpod/cmd/podman/cliconfig" "github.com/containers/libpod/cmd/podman/shared" @@ -148,6 +151,54 @@ func (r *LocalRuntime) StopContainers(ctx context.Context, cli *cliconfig.StopVa return ok, failures, nil } +// KillContainers sends signal to container(s) based on CLI inputs. +// Returns list of successful id(s), map of failed id(s) + error, or error not from container +func (r *LocalRuntime) KillContainers(ctx context.Context, cli *cliconfig.KillValues, signal syscall.Signal) ([]string, map[string]error, error) { + var ( + ok = []string{} + failures = map[string]error{} + ) + + ids, err := iopodman.GetContainersByContext().Call(r.Conn, cli.All, cli.Latest, cli.InputArgs) + if err != nil { + return ok, failures, err + } + + for _, id := range ids { + killed, err := iopodman.KillContainer().Call(r.Conn, id, int64(signal)) + if err != nil { + failures[id] = err + } else { + ok = append(ok, killed) + } + } + return ok, failures, nil +} + +// WaitOnContainers waits for all given container(s) to stop. +// interval is currently ignored. +func (r *LocalRuntime) WaitOnContainers(ctx context.Context, cli *cliconfig.WaitValues, interval time.Duration) ([]string, map[string]error, error) { + var ( + ok = []string{} + failures = map[string]error{} + ) + + ids, err := iopodman.GetContainersByContext().Call(r.Conn, false, cli.Latest, cli.InputArgs) + if err != nil { + return ok, failures, err + } + + for _, id := range ids { + stopped, err := iopodman.WaitContainer().Call(r.Conn, id, int64(interval)) + if err != nil { + failures[id] = err + } else { + ok = append(ok, strconv.FormatInt(stopped, 10)) + } + } + return ok, failures, nil +} + // BatchContainerOp is wrapper func to mimic shared's function with a similar name meant for libpod func BatchContainerOp(ctr *Container, opts shared.PsOptions) (shared.BatchContainerStruct, error) { // TODO If pod ps ever shows container's sizes, re-enable this code; otherwise it isn't needed diff --git a/pkg/adapter/pods.go b/pkg/adapter/pods.go index 706a8fe96..669971789 100644 --- a/pkg/adapter/pods.go +++ b/pkg/adapter/pods.go @@ -4,6 +4,7 @@ package adapter import ( "context" + "github.com/pkg/errors" "strings" "github.com/containers/libpod/cmd/podman/cliconfig" @@ -17,6 +18,13 @@ type Pod struct { *libpod.Pod } +// PodContainerStats is struct containing an adapter Pod and a libpod +// ContainerStats and is used primarily for outputing pod stats. +type PodContainerStats struct { + Pod *Pod + ContainerStats map[string]*libpod.ContainerStats +} + // RemovePods ... func (r *LocalRuntime) RemovePods(ctx context.Context, cli *cliconfig.PodRmValues) ([]string, []error) { var ( @@ -321,3 +329,55 @@ func (r *LocalRuntime) RestartPods(ctx context.Context, c *cliconfig.PodRestartV return restartIDs, containerErrors, restartErrors } + +// PodTop is a wrapper function to call GetPodPidInformation in libpod and return its results +// for output +func (r *LocalRuntime) PodTop(c *cliconfig.PodTopValues, descriptors []string) ([]string, error) { + var ( + pod *Pod + err error + ) + + if c.Latest { + pod, err = r.GetLatestPod() + } else { + pod, err = r.LookupPod(c.InputArgs[0]) + } + if err != nil { + return nil, errors.Wrapf(err, "unable to lookup requested container") + } + podStatus, err := pod.GetPodStatus() + if err != nil { + return nil, errors.Wrapf(err, "unable to get status for pod %s", pod.ID()) + } + if podStatus != "Running" { + return nil, errors.Errorf("pod top can only be used on pods with at least one running container") + } + return pod.GetPodPidInformation(descriptors) +} + +// GetStatPods returns pods for use in pod stats +func (r *LocalRuntime) GetStatPods(c *cliconfig.PodStatsValues) ([]*Pod, error) { + var ( + adapterPods []*Pod + pods []*libpod.Pod + err error + ) + + if len(c.InputArgs) > 0 || c.Latest || c.All { + pods, err = shortcuts.GetPodsByContext(c.All, c.Latest, c.InputArgs, r.Runtime) + } else { + pods, err = r.Runtime.GetRunningPods() + } + if err != nil { + return nil, err + } + // convert libpod pods to adapter pods + for _, p := range pods { + adapterPod := Pod{ + p, + } + adapterPods = append(adapterPods, &adapterPod) + } + return adapterPods, nil +} diff --git a/pkg/adapter/pods_remote.go b/pkg/adapter/pods_remote.go index 220f7163f..ef8de90a6 100644 --- a/pkg/adapter/pods_remote.go +++ b/pkg/adapter/pods_remote.go @@ -12,6 +12,7 @@ import ( "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/cmd/podman/varlink" "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/varlinkapi" "github.com/pkg/errors" "github.com/ulule/deepcopier" ) @@ -21,6 +22,13 @@ type Pod struct { remotepod } +// PodContainerStats is struct containing an adapter Pod and a libpod +// ContainerStats and is used primarily for outputing pod stats. +type PodContainerStats struct { + Pod *Pod + ContainerStats map[string]*libpod.ContainerStats +} + type remotepod struct { config *libpod.PodConfig state *libpod.PodInspectState @@ -399,3 +407,103 @@ func (r *LocalRuntime) RestartPods(ctx context.Context, c *cliconfig.PodRestartV } return restartIDs, nil, restartErrors } + +// PodTop gets top statistics for a pod +func (r *LocalRuntime) PodTop(c *cliconfig.PodTopValues, descriptors []string) ([]string, error) { + var ( + latest bool + podName string + ) + if c.Latest { + latest = true + } else { + podName = c.InputArgs[0] + } + return iopodman.TopPod().Call(r.Conn, podName, latest, descriptors) +} + +// GetStatPods returns pods for use in pod stats +func (r *LocalRuntime) GetStatPods(c *cliconfig.PodStatsValues) ([]*Pod, error) { + var ( + pods []*Pod + err error + podIDs []string + running bool + ) + + if len(c.InputArgs) > 0 || c.Latest || c.All { + podIDs, err = iopodman.GetPodsByContext().Call(r.Conn, c.All, c.Latest, c.InputArgs) + } else { + podIDs, err = iopodman.GetPodsByContext().Call(r.Conn, true, false, []string{}) + running = true + } + if err != nil { + return nil, err + } + for _, p := range podIDs { + pod, err := r.Inspect(p) + if err != nil { + return nil, err + } + if running { + status, err := pod.GetPodStatus() + if err != nil { + // if we cannot get the status of the pod, skip and move on + continue + } + if strings.ToUpper(status) != "RUNNING" { + // if the pod is not running, skip and move on as well + continue + } + } + pods = append(pods, pod) + } + return pods, nil +} + +// GetPodStats returns the stats for each of its containers +func (p *Pod) GetPodStats(previousContainerStats map[string]*libpod.ContainerStats) (map[string]*libpod.ContainerStats, error) { + var ( + ok bool + prevStat *libpod.ContainerStats + ) + newContainerStats := make(map[string]*libpod.ContainerStats) + containers, err := p.AllContainers() + if err != nil { + return nil, err + } + for _, c := range containers { + if prevStat, ok = previousContainerStats[c.ID()]; !ok { + prevStat = &libpod.ContainerStats{ContainerID: c.ID()} + } + cStats := iopodman.ContainerStats{ + Id: prevStat.ContainerID, + Name: prevStat.Name, + Cpu: prevStat.CPU, + Cpu_nano: int64(prevStat.CPUNano), + System_nano: int64(prevStat.SystemNano), + Mem_usage: int64(prevStat.MemUsage), + Mem_limit: int64(prevStat.MemLimit), + Mem_perc: prevStat.MemPerc, + Net_input: int64(prevStat.NetInput), + Net_output: int64(prevStat.NetOutput), + Block_input: int64(prevStat.BlockInput), + Block_output: int64(prevStat.BlockOutput), + Pids: int64(prevStat.PIDs), + } + stats, err := iopodman.GetContainerStatsWithHistory().Call(p.Runtime.Conn, cStats) + if err != nil { + return nil, err + } + newStats := varlinkapi.ContainerStatsToLibpodContainerStats(stats) + // If the container wasn't running, don't include it + // but also suppress the error + if err != nil && errors.Cause(err) != libpod.ErrCtrStateInvalid { + return nil, err + } + if err == nil { + newContainerStats[c.ID()] = &newStats + } + } + return newContainerStats, 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/rootless/rootless_linux.c b/pkg/rootless/rootless_linux.c index dfbc7fe33..41acd3475 100644 --- a/pkg/rootless/rootless_linux.c +++ b/pkg/rootless/rootless_linux.c @@ -32,7 +32,11 @@ syscall_setresgid (gid_t rgid, gid_t egid, gid_t sgid) static int syscall_clone (unsigned long flags, void *child_stack) { +#if defined(__s390__) || defined(__CRIS__) + return (int) syscall (__NR_clone, child_stack, flags); +#else return (int) syscall (__NR_clone, flags, child_stack); +#endif } static char ** diff --git a/pkg/spec/config_linux.go b/pkg/spec/config_linux.go index eccd41ff9..a1873086e 100644 --- a/pkg/spec/config_linux.go +++ b/pkg/spec/config_linux.go @@ -46,19 +46,32 @@ func devicesFromPath(g *generate.Generator, devicePath string) error { return errors.Wrapf(err, "cannot stat %s", devicePath) } if st.IsDir() { + found := false + src := resolvedDevicePath + dest := src + var devmode string + if len(devs) > 1 { + if len(devs[1]) > 0 && devs[1][0] == '/' { + dest = devs[1] + } else { + devmode = devs[1] + } + } if len(devs) > 2 { - return errors.Wrapf(unix.EINVAL, "not allowed to specify destination with a directory %s", devicePath) + if devmode != "" { + return errors.Wrapf(unix.EINVAL, "invalid device specification %s", devicePath) + } + devmode = devs[2] } - found := false + // mount the internal devices recursively if err := filepath.Walk(resolvedDevicePath, func(dpath string, f os.FileInfo, e error) error { if f.Mode()&os.ModeDevice == os.ModeDevice { found = true - device := dpath - - if len(devs) > 1 { - device = fmt.Sprintf("%s:%s", dpath, devs[1]) + device := fmt.Sprintf("%s:%s", dpath, filepath.Join(dest, strings.TrimPrefix(dpath, src))) + if devmode != "" { + device = fmt.Sprintf("%s:%s", device, devmode) } if err := addDevice(g, device); err != nil { return errors.Wrapf(err, "failed to add %s device", dpath) 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/pkg/varlinkapi/containers.go b/pkg/varlinkapi/containers.go index 27b8d15d2..fe38a7cdc 100644 --- a/pkg/varlinkapi/containers.go +++ b/pkg/varlinkapi/containers.go @@ -360,17 +360,16 @@ func (i *LibpodAPI) UnpauseContainer(call iopodman.VarlinkCall, name string) err } // WaitContainer ... -func (i *LibpodAPI) WaitContainer(call iopodman.VarlinkCall, name string) error { +func (i *LibpodAPI) WaitContainer(call iopodman.VarlinkCall, name string, interval int64) error { ctr, err := i.Runtime.LookupContainer(name) if err != nil { return call.ReplyContainerNotFound(name, err.Error()) } - exitCode, err := ctr.Wait() + exitCode, err := ctr.WaitWithInterval(time.Duration(interval)) if err != nil { return call.ReplyErrorOccurred(err.Error()) } return call.ReplyWaitContainer(int64(exitCode)) - } // RemoveContainer ... @@ -552,3 +551,54 @@ func (i *LibpodAPI) ContainerStateData(call iopodman.VarlinkCall, name string) e } return call.ReplyContainerStateData(string(b)) } + +// GetContainerStatsWithHistory is a varlink endpoint that returns container stats based on current and +// previous statistics +func (i *LibpodAPI) GetContainerStatsWithHistory(call iopodman.VarlinkCall, prevStats iopodman.ContainerStats) error { + con, err := i.Runtime.LookupContainer(prevStats.Id) + if err != nil { + return call.ReplyContainerNotFound(prevStats.Id, err.Error()) + } + previousStats := ContainerStatsToLibpodContainerStats(prevStats) + stats, err := con.GetContainerStats(&previousStats) + if err != nil { + return call.ReplyErrorOccurred(err.Error()) + } + cStats := iopodman.ContainerStats{ + Id: stats.ContainerID, + Name: stats.Name, + Cpu: stats.CPU, + Cpu_nano: int64(stats.CPUNano), + System_nano: int64(stats.SystemNano), + Mem_usage: int64(stats.MemUsage), + Mem_limit: int64(stats.MemLimit), + Mem_perc: stats.MemPerc, + Net_input: int64(stats.NetInput), + Net_output: int64(stats.NetOutput), + Block_input: int64(stats.BlockInput), + Block_output: int64(stats.BlockOutput), + Pids: int64(stats.PIDs), + } + return call.ReplyGetContainerStatsWithHistory(cStats) +} + +// ContainerStatsToLibpodContainerStats converts the varlink containerstats to a libpod +// container stats +func ContainerStatsToLibpodContainerStats(stats iopodman.ContainerStats) libpod.ContainerStats { + cstats := libpod.ContainerStats{ + ContainerID: stats.Id, + Name: stats.Name, + CPU: stats.Cpu, + CPUNano: uint64(stats.Cpu_nano), + SystemNano: uint64(stats.System_nano), + MemUsage: uint64(stats.Mem_usage), + MemLimit: uint64(stats.Mem_limit), + MemPerc: stats.Mem_perc, + NetInput: uint64(stats.Net_input), + NetOutput: uint64(stats.Net_output), + BlockInput: uint64(stats.Block_input), + BlockOutput: uint64(stats.Block_output), + PIDs: uint64(stats.Pids), + } + return cstats +} diff --git a/pkg/varlinkapi/pods.go b/pkg/varlinkapi/pods.go index 4ca4c4270..c79cee4c2 100644 --- a/pkg/varlinkapi/pods.go +++ b/pkg/varlinkapi/pods.go @@ -2,6 +2,7 @@ package varlinkapi import ( "encoding/json" + "fmt" "github.com/containers/libpod/pkg/adapter/shortcuts" "github.com/containers/libpod/pkg/rootless" "syscall" @@ -299,3 +300,33 @@ func (i *LibpodAPI) PodStateData(call iopodman.VarlinkCall, name string) error { } return call.ReplyPodStateData(string(b)) } + +// TopPod provides the top stats for a given or latest pod +func (i *LibpodAPI) TopPod(call iopodman.VarlinkCall, name string, latest bool, descriptors []string) error { + var ( + pod *libpod.Pod + err error + ) + if latest { + name = "latest" + pod, err = i.Runtime.GetLatestPod() + } else { + pod, err = i.Runtime.LookupPod(name) + } + if err != nil { + return call.ReplyPodNotFound(name, err.Error()) + } + + podStatus, err := shared.GetPodStatus(pod) + if err != nil { + return call.ReplyErrorOccurred(fmt.Sprintf("unable to get status for pod %s", pod.ID())) + } + if podStatus != "Running" { + return call.ReplyErrorOccurred("pod top can only be used on pods with at least one running container") + } + reply, err := pod.GetPodPidInformation(descriptors) + if err != nil { + return call.ReplyErrorOccurred(err.Error()) + } + return call.ReplyTopPod(reply) +} 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/test/e2e/images_test.go b/test/e2e/images_test.go index e26f4affd..4cf58e5bf 100644 --- a/test/e2e/images_test.go +++ b/test/e2e/images_test.go @@ -114,6 +114,33 @@ var _ = Describe("Podman images", func() { Expect(len(session.OutputToStringArray())).To(Equal(1)) }) + It("podman images filter reference", func() { + if podmanTest.RemoteTest { + Skip("Does not work on remote client") + } + result := podmanTest.Podman([]string{"images", "-q", "-f", "reference=docker.io*"}) + result.WaitWithDefaultTimeout() + Expect(result.ExitCode()).To(Equal(0)) + Expect(len(result.OutputToStringArray())).To(Equal(2)) + + retapline := podmanTest.Podman([]string{"images", "-f", "reference=a*pine"}) + retapline.WaitWithDefaultTimeout() + Expect(retapline.ExitCode()).To(Equal(0)) + Expect(len(retapline.OutputToStringArray())).To(Equal(2)) + Expect(retapline.LineInOutputContains("alpine")) + + retapline = podmanTest.Podman([]string{"images", "-f", "reference=alpine"}) + retapline.WaitWithDefaultTimeout() + Expect(retapline.ExitCode()).To(Equal(0)) + Expect(len(retapline.OutputToStringArray())).To(Equal(2)) + Expect(retapline.LineInOutputContains("alpine")) + + retnone := podmanTest.Podman([]string{"images", "-q", "-f", "reference=bogus"}) + retnone.WaitWithDefaultTimeout() + Expect(retnone.ExitCode()).To(Equal(0)) + Expect(len(retnone.OutputToStringArray())).To(Equal(0)) + }) + It("podman images filter before image", func() { if podmanTest.RemoteTest { Skip("Does not work on remote client") diff --git a/test/e2e/pod_infra_container_test.go b/test/e2e/pod_infra_container_test.go index ed5002ca7..d9e5d380a 100644 --- a/test/e2e/pod_infra_container_test.go +++ b/test/e2e/pod_infra_container_test.go @@ -360,4 +360,21 @@ var _ = Describe("Podman pod create", func() { Expect(result.OutputToString()).To(ContainSubstring(infraID)) }) + + It("podman run --add-host in pod", func() { + session := podmanTest.Podman([]string{"pod", "create"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + podID := session.OutputToString() + + // verify we can add a host to the infra's /etc/hosts + session = podmanTest.Podman([]string{"run", "--pod", podID, "--add-host", "foobar:127.0.0.1", BB, "ping", "-c", "1", "foobar"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + // verify we can see the other hosts of infra's /etc/hosts + session = podmanTest.Podman([]string{"run", "--pod", podID, BB, "ping", "-c", "1", "foobar"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + }) }) diff --git a/test/e2e/run_device_test.go b/test/e2e/run_device_test.go index 4f26ac8ee..8734bb71c 100644 --- a/test/e2e/run_device_test.go +++ b/test/e2e/run_device_test.go @@ -72,4 +72,12 @@ var _ = Describe("Podman run device", func() { session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Not(Equal(0))) }) + + It("podman run device host device and container device parameter are directories", func() { + SystemExec("mkdir", []string{"/dev/foodevdir"}) + SystemExec("mknod", []string{"/dev/foodevdir/null", "c", "1", "3"}) + session := podmanTest.Podman([]string{"run", "-q", "--device", "/dev/foodevdir:/dev/bar", ALPINE, "ls", "/dev/bar/null"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + }) }) diff --git a/test/system/000-TEMPLATE b/test/system/000-TEMPLATE new file mode 100644 index 000000000..296ed4d58 --- /dev/null +++ b/test/system/000-TEMPLATE @@ -0,0 +1,114 @@ +#!/usr/bin/env bats -*- bats -*- +# +# FIXME: short description of the purpose of this module +# +# FIXME: copy this file to 'NNN-yourtestname.bats' and edit as needed. +# + +load helpers + +@test "podman subcmd - description of this particular test" { + args="some sort of argument list" + run_podman subcmd $args + is "$output" "what we expect" "output from 'podman subcmd $args'" +} + +# vim: filetype=sh + +############################################################################### +# +# FIXME FIXME FIXME: Most of the time you can cut from here on down. +# FIXME FIXME FIXME: The above template is probably enough for many tests. +# FIXME FIXME FIXME: +# FIXME FIXME FIXME: If you need anything more complicated, read on. +# +# FIXME: This is a bloated test template. It provides mostly stuff for you +# FIXME: to remove, plus stuff for you to base your tests on. +# FIXME: +# FIXME: copy this file to 'NNN-yourtestname.bats' and edit as needed. +# FIXME: Read all FIXMEs, act on them as needed, then remove them. +# FIXME: test w/ $ PODMAN=./bin/podman bats test/system/NNN-yourtestname.bats +# + +load helpers + +# FIXME: DELETE THESE LINES UNLESS YOU ABSOLUTELY NEED THEM. +# FIXME: Most tests will not need a custom setup/teardown: they are +# FIXME: provided by helpers.bash. +# FIXME: But if you have to do anything special, these give you the +# FIXME: names of the standard setup/teardown so you can call them +# FIXME: before or after your own additions. +function setup() { + basic_setup + # FIXME: you almost certainly want to do your own setup _after_ basic. +} +function teardown() { + # FIXME: you almost certainly want to do your own teardown _before_ basic. + basic_teardown +} + + +# FIXME: very basic one-pass example +@test "podman FOO - description of test" { + # FIXME: please try to remove this line; that is, try to write tests + # that will pass as both root and rootless. + skip_if_rootless + + # FIXME: template for run commands. Always use 'run_podman'! + # FIXME: The '?' means 'ignore exit status'; use a number if you + # FIXME: expect a precise nonzero code, or omit for 0 (usual case). + # FIXME: NEVER EVER RUN 'podman' DIRECTLY. See helpers.bash for why. + run_podman '?' run -d $IMAGE sh -c 'prep..; echo READY' + cid="$output" + wait_for_ready $cid + + run_podman logs $cid + # FIXME: example of dprint. This will trigger if PODMAN_TEST_DEBUG=FOO + # FIXME: ...or anything that matches the name assigned in the @test line. + dprint "podman logs $cid -> '$output'" + is "$output" "what are we expecting?" "description of this check" + + # Clean up + run_podman rm $cid +} + + +# FIXME: another example, this time with a test table loop +@test "podman FOO - json - template for playing with json output" { + # FIXME: Define a multiline string in tabular form, using '|' as separator. + # FIXME: Each row defines one test. Each column (there may be as many as + # FIXME: you want) is one field. In the case below we have two, a + # FIXME: json field descriptor and an expected value. + tests=" +id | [0-9a-f]\\\{64\\\} +created | [0-9-]\\\+T[0-9:]\\\+\\\.[0-9]\\\+Z +size | -\\\?[0-9]\\\+ +" + + # FIXME: Run a basic podman command. We'll check $output multiple times + # FIXME: in the while loop below. + run_podman history --format json $IMAGE + + # FIXME: parse_table is what does all the work, giving us test cases. + parse_table "$tests" | while read field expect; do + # FIXME: this shows a drawback of BATS and bash: we can't include '|' + # FIXME: in the table, but we need to because some images don't + # FIXME: have a CID. So, yeah, this is ugly -- but rare. + if [ "$field" = "id" ]; then expect="$expect\|<missing>";fi + + # output is an array of dicts; check each one + count=$(echo "$output" | jq '. | length') + i=0 + while [ $i -lt $count ]; do + actual=$(echo "$output" | jq -r ".[$i].$field") + # FIXME: please be sure to note the third field! + # FIXME: that's the test name. Make it something useful! Include + # FIXME: loop variables whenever possible. Don't just say "my test" + is "$actual" "$expect\$" "jq .[$i].$field" + i=$(expr $i + 1) + done + done +} + + +# vim: filetype=sh diff --git a/test/system/001-basic.bats b/test/system/001-basic.bats new file mode 100644 index 000000000..85b9bc1ca --- /dev/null +++ b/test/system/001-basic.bats @@ -0,0 +1,50 @@ +#!/usr/bin/env bats +# +# Simplest set of podman tests. If any of these fail, we have serious problems. +# + +load helpers + +# Override standard setup! We don't yet trust podman-images or podman-rm +function setup() { + : +} + +@test "podman version emits reasonable output" { + run_podman version + + is "${lines[0]}" "Version:[ ]\+[1-9][0-9.]\+" "Version line 1" + is "$output" ".*Go Version: \+" "'Go Version' in output" + is "$output" ".*RemoteAPI Version: \+" "API version in output" +} + + +@test "podman can pull an image" { + run_podman pull $IMAGE +} + +# This is for development only; it's intended to make sure our timeout +# in run_podman continues to work. This test should never run in production +# because it will, by definition, fail. +@test "timeout" { + if [ -z "$PODMAN_RUN_TIMEOUT_TEST" ]; then + skip "define \$PODMAN_RUN_TIMEOUT_TEST to enable this test" + fi + PODMAN_TIMEOUT=10 run_podman run $IMAGE sleep 90 + echo "*** SHOULD NEVER GET HERE" +} + + +# Too many tests rely on jq for parsing JSON. +# +# If absolutely necessary, one could establish a convention such as +# defining PODMAN_TEST_SKIP_JQ=1 and adding a skip_if_no_jq() helper. +# For now, let's assume this is not absolutely necessary. +@test "jq is installed and produces reasonable output" { + type -path jq >/dev/null || die "FATAL: 'jq' tool not found." + + run jq -r .a.b < <(echo '{ "a": { "b" : "you found me" } }') + is "$output" "you found me" "sample invocation of 'jq'" +} + +# vim: filetype=sh diff --git a/test/system/005-info.bats b/test/system/005-info.bats new file mode 100644 index 000000000..7dcc78838 --- /dev/null +++ b/test/system/005-info.bats @@ -0,0 +1,54 @@ +#!/usr/bin/env bats + +load helpers + +@test "podman info - basic test" { + run_podman info + + expected_keys=" +BuildahVersion: *[0-9.]\\\+ +Conmon:\\\s\\\+package: +Distribution: +OCIRuntime:\\\s\\\+package: +os: +rootless: +insecure registries: +store: +GraphDriverName: +GraphRoot: +GraphStatus: +ImageStore:\\\s\\\+number: 1 +RunRoot: +" + while read expect; do + is "$output" ".*$expect" "output includes '$expect'" + done < <(parse_table "$expected_keys") +} + +@test "podman info - json" { + run_podman info --format=json + + expr_nvr="[a-z0-9-]\\\+-[a-z0-9.]\\\+-[a-z0-9]\\\+\." + expr_path="/[a-z0-9\\\/.]\\\+\\\$" + + tests=" +host.BuildahVersion | [0-9.] +host.Conmon.package | $expr_nvr +host.Conmon.path | $expr_path +host.OCIRuntime.package | $expr_nvr +host.OCIRuntime.path | $expr_path +store.ConfigFile | $expr_path +store.GraphDriverName | [a-z0-9]\\\+\\\$ +store.GraphRoot | $expr_path +store.ImageStore.number | 1 +" + + parse_table "$tests" | while read field expect; do + actual=$(echo "$output" | jq -r ".$field") + dprint "# actual=<$actual> expect=<$expect>" + is "$actual" "$expect" "jq .$field" + done + +} + +# vim: filetype=sh diff --git a/test/system/010-images.bats b/test/system/010-images.bats new file mode 100644 index 000000000..1c9577e34 --- /dev/null +++ b/test/system/010-images.bats @@ -0,0 +1,46 @@ +#!/usr/bin/env bats + +load helpers + +@test "podman images - basic output" { + run_podman images -a + + is "${lines[0]}" "REPOSITORY *TAG *IMAGE ID *CREATED *SIZE" "header line" + is "${lines[1]}" "$PODMAN_TEST_IMAGE_REGISTRY/$PODMAN_TEST_IMAGE_USER/$PODMAN_TEST_IMAGE_NAME *$PODMAN_TEST_IMAGE_TAG *[0-9a-f]\+" "podman images output" +} + +@test "podman images - custom formats" { + tests=" +--format {{.ID}} | [0-9a-f]\\\{12\\\} +--format {{.ID}} --no-trunc | sha256:[0-9a-f]\\\{64\\\} +--format {{.Repository}}:{{.Tag}} | $PODMAN_TEST_IMAGE_FQN +" + + parse_table "$tests" | while read fmt expect; do + run_podman images $fmt + is "$output" "$expect\$" "podman images $fmt" + done + +} + + +@test "podman images - json" { + tests=" +names[0] | $PODMAN_TEST_IMAGE_FQN +id | [0-9a-f]\\\{64\\\} +digest | sha256:[0-9a-f]\\\{64\\\} +created | [0-9-]\\\+T[0-9:]\\\+\\\.[0-9]\\\+Z +size | [0-9]\\\+ +" + + run_podman images -a --format json + + parse_table "$tests" | while read field expect; do + actual=$(echo "$output" | jq -r ".[0].$field") + dprint "# actual=<$actual> expect=<$expect}>" + is "$actual" "$expect" "jq .$field" + done + +} + +# vim: filetype=sh diff --git a/test/system/015-help.bats b/test/system/015-help.bats new file mode 100644 index 000000000..b648599f7 --- /dev/null +++ b/test/system/015-help.bats @@ -0,0 +1,76 @@ +#!/usr/bin/env bats +# +# Tests based on 'podman help' +# +# Find all commands listed by 'podman --help'. Run each one, make sure it +# provides its own --help output. If the usage message ends in '[command]', +# treat it as a subcommand, and recurse into its own list of sub-subcommands. +# +# Any usage message that ends in '[flags]' is interpreted as a command +# that takes no further arguments; we confirm by running with 'invalid-arg' +# and confirming that it exits with error status and message. +# +load helpers + +# run 'podman help', parse the output looking for 'Available Commands'; +# return that list. +function podman_commands() { + dprint "$@" + run_podman help "$@" |\ + awk '/^Available Commands:/{ok=1;next}/^Flags:/{ok=0}ok { print $1 }' |\ + grep . + "$output" +} + + +function check_help() { + local count=0 + local subcommands_found=0 + + for cmd in $(podman_commands "$@"); do + dprint "podman $@ $cmd --help" + run_podman "$@" $cmd --help + + # The line immediately after 'Usage:' gives us a 1-line synopsis + usage=$(echo "$output" | grep -A1 '^Usage:' | tail -1) + [ -n "$usage" ] || die "podman $cmd: no Usage message found" + + # If usage ends in '[command]', recurse into subcommands + if expr "$usage" : '.*\[command\]$' >/dev/null; then + subcommands_found=$(expr $subcommands_found + 1) + check_help "$@" $cmd + continue + fi + + # If usage ends in '[flag]', command takes no more arguments. + # Confirm that by running with 'invalid-arg' and expecting failure. + if expr "$usage" : '.*\[flags\]$' >/dev/null; then + if [ "$cmd" != "help" ]; then + run_podman 125 "$@" $cmd invalid-arg + is "$output" "Error: .* takes no arguments" \ + "'podman $@ $cmd' with extra (invalid) arguments" + fi + fi + + count=$(expr $count + 1) + done + + # This can happen if the output of --help changes, such as between + # the old command parser and cobra. + [ $count -gt 0 ] || \ + die "Internal error: no commands found in 'podman help $@' list" + + # At least the top level must have some subcommands + if [ -z "$*" -a $subcommands_found -eq 0 ]; then + die "Internal error: did not find any podman subcommands" + fi +} + + +@test "podman help - basic tests" { + # Called with no args -- start with 'podman --help'. check_help() will + # recurse for any subcommands. + check_help +} + +# vim: filetype=sh diff --git a/test/system/030-run.bats b/test/system/030-run.bats new file mode 100644 index 000000000..8ae68f33d --- /dev/null +++ b/test/system/030-run.bats @@ -0,0 +1,34 @@ +#!/usr/bin/env bats + +load helpers + +@test "podman run - basic tests" { + rand=$(random_string 30) + tests=" +true | 0 | +false | 1 | +sh -c 'exit 32' | 32 | +echo $rand | 0 | $rand +/no/such/command | 127 | Error: container create failed:.*exec:.* no such file or dir +/etc | 126 | Error: container create failed:.*exec:.* permission denied +" + + while read cmd expected_rc expected_output; do + if [ "$expected_output" = "''" ]; then expected_output=""; fi + + # THIS IS TRICKY: this is what lets us handle a quoted command. + # Without this incantation (and the "$@" below), the cmd string + # gets passed on as individual tokens: eg "sh" "-c" "'exit" "32'" + # (note unmatched opening and closing single-quotes in the last 2). + # That results in a bizarre and hard-to-understand failure + # in the BATS 'run' invocation. + # This should really be done inside parse_table; I can't find + # a way to do so. + eval set "$cmd" + + run_podman $expected_rc run $IMAGE "$@" + is "$output" "$expected_output" "podman run $cmd - output" + done < <(parse_table "$tests") +} + +# vim: filetype=sh diff --git a/test/system/035-logs.bats b/test/system/035-logs.bats new file mode 100644 index 000000000..debec29b6 --- /dev/null +++ b/test/system/035-logs.bats @@ -0,0 +1,24 @@ +#!/usr/bin/env bats -*- bats -*- +# +# Basic tests for podman logs +# + +load helpers + +@test "podman logs - basic test" { + rand_string=$(random_string 40) + + run_podman create $IMAGE echo $rand_string + cid="$output" + + run_podman logs $cid + is "$output" "" "logs on created container: empty" + + run_podman start --attach --interactive $cid + is "$output" "$rand_string" "output from podman-start on created ctr" + is "$output" "$rand_string" "logs of started container" + + run_podman rm $cid +} + +# vim: filetype=sh diff --git a/test/system/040-ps.bats b/test/system/040-ps.bats new file mode 100644 index 000000000..dec2df4d5 --- /dev/null +++ b/test/system/040-ps.bats @@ -0,0 +1,38 @@ +#!/usr/bin/env bats + +load helpers + +@test "podman ps - basic tests" { + rand_name=$(random_string 30) + + run_podman run -d --name $rand_name $IMAGE sleep 5 + cid=$output + is "$cid" "[0-9a-f]\{64\}$" + + # Special case: formatted ps + run_podman ps --no-trunc \ + --format '{{.ID}} {{.Image}} {{.Command}} {{.Names}}' + is "$output" "$cid $IMAGE sleep 5 $rand_name" "podman ps" + + + # Plain old regular ps + run_podman ps + is "${lines[1]}" \ + "${cid:0:12} \+$IMAGE \+sleep [0-9]\+ .*second.* $cname"\ + "output from podman ps" + + # OK. Wait for sleep to finish... + run_podman wait $cid + + # ...then make sure container shows up as stopped + run_podman ps -a + is "${lines[1]}" \ + "${cid:0:12} \+$IMAGE *sleep .* Exited .* $rand_name" \ + "podman ps -a" + + + + run_podman rm $cid +} + +# vim: filetype=sh diff --git a/test/system/050-stop.bats b/test/system/050-stop.bats new file mode 100644 index 000000000..093606ece --- /dev/null +++ b/test/system/050-stop.bats @@ -0,0 +1,67 @@ +#!/usr/bin/env bats + +load helpers + +# Very simple test +@test "podman stop - basic test" { + run_podman run -d $IMAGE sleep 60 + cid="$output" + + # Run 'stop'. Time how long it takes. + t0=$SECONDS + run_podman stop $cid + t1=$SECONDS + + # Confirm that container is stopped + run_podman inspect --format '{{.State.Status}} {{.State.ExitCode}}' $cid + is "$output" "exited \+137" "Status and exit code of stopped container" + + # The initial SIGTERM is ignored, so this operation should take + # exactly 10 seconds. Give it some leeway. + delta_t=$(( $t1 - $t0 )) + [ $delta_t -gt 8 ] ||\ + die "podman stop: ran too quickly! ($delta_t seconds; expected >= 10)" + [ $delta_t -le 14 ] ||\ + die "podman stop: took too long ($delta_t seconds; expected ~10)" + + run_podman rm $cid +} + + +# Test fallback + + +# Regression test for #2472 +@test "podman stop - can trap signal" { + # Because the --time and --timeout options can be wonky, try three + # different variations of this test. + for t_opt in '' '--time=5' '--timeout=5'; do + # Run a simple container that logs output on SIGTERM + run_podman run -d $IMAGE sh -c \ + "trap 'echo Received SIGTERM, finishing; exit' SIGTERM; echo READY; while :; do sleep 1; done" + cid="$output" + wait_for_ready $cid + + # Run 'stop' against it... + t0=$SECONDS + run_podman stop $t_opt $cid + t1=$SECONDS + + # ...the container should trap the signal, log it, and exit. + run_podman logs $cid + is "$output" ".*READY.*Received SIGTERM, finishing" "podman stop $t_opt" + + # Exit code should be 0, because container did its own exit + run_podman inspect --format '{{.State.ExitCode}}' $cid + is "$output" "0" "Exit code of stopped container" + + # The 'stop' command should return almost instantaneously + delta_t=$(( $t1 - $t0 )) + [ $delta_t -le 2 ] ||\ + die "podman stop: took too long ($delta_t seconds; expected <= 2)" + + run_podman rm $cid + done +} + +# vim: filetype=sh diff --git a/test/system/060-mount.bats b/test/system/060-mount.bats new file mode 100644 index 000000000..3601b7b84 --- /dev/null +++ b/test/system/060-mount.bats @@ -0,0 +1,37 @@ +#!/usr/bin/env bats + +load helpers + + +@test "podman mount - basic test" { + # Only works with root (FIXME: does it work with rootless + vfs?) + skip_if_rootless + + f_path=/tmp/tmpfile_$(random_string 8) + f_content=$(random_string 30) + + c_name=mount_test_$(random_string 5) + run_podman run --name $c_name $IMAGE \ + sh -c "echo $f_content > $f_path" + + run_podman mount $c_name + mount_path=$output + + test -d $mount_path + test -e "$mount_path/$f_path" + is $(< "$mount_path/$f_path") "$f_content" "contents of file, as read via fs" + + # Make sure that 'podman mount' (no args) returns the expected path + run_podman mount --notruncate + # FIXME: is it worth the effort to validate the CID ($1) ? + reported_mountpoint=$(echo "$output" | awk '{print $2}') + is $reported_mountpoint $mount_path "mountpoint reported by 'podman mount'" + + # umount, and make sure files are gone + run_podman umount $c_name + if [ -e "$mount_path/$f_path" ]; then + die "Mounted file exists even after umount: $mount_path/$f_path" + fi +} + +# vim: filetype=sh diff --git a/test/system/070-build.bats b/test/system/070-build.bats new file mode 100644 index 000000000..25eb36c58 --- /dev/null +++ b/test/system/070-build.bats @@ -0,0 +1,28 @@ +#!/usr/bin/env bats -*- bats -*- +# +# Tests for podman build +# + +load helpers + +@test "podman build - basic test" { + rand_filename=$(random_string 20) + rand_content=$(random_string 50) + + tmpdir=$PODMAN_TMPDIR/build-test + run mkdir -p $tmpdir || die "Could not mkdir $tmpdir" + dockerfile=$tmpdir/Dockerfile + cat >$dockerfile <<EOF +FROM $IMAGE +RUN echo $rand_content > /$rand_filename +EOF + + run_podman build -t build_test --format=docker $tmpdir + + run_podman run --rm build_test cat /$rand_filename + is "$output" "$rand_content" "reading generated file in image" + + run_podman rmi build_test +} + +# vim: filetype=sh diff --git a/test/system/110-history.bats b/test/system/110-history.bats new file mode 100644 index 000000000..84a1e42b4 --- /dev/null +++ b/test/system/110-history.bats @@ -0,0 +1,49 @@ +#!/usr/bin/env bats + +load helpers + +@test "podman history - basic tests" { + tests=" + | .*[0-9a-f]\\\{12\\\} .* CMD .* LABEL +--format '{{.ID}} {{.Created}}' | .*[0-9a-f]\\\{12\\\} .* ago +--human=false | .*[0-9a-f]\\\{12\\\} *[0-9-]\\\+T[0-9:]\\\+Z +-qH | .*[0-9a-f]\\\{12\\\} +--no-trunc | .*[0-9a-f]\\\{64\\\} +" + + parse_table "$tests" | while read options expect; do + if [ "$options" = "''" ]; then options=; fi + + eval set -- "$options" + + run_podman history "$@" $IMAGE + is "$output" "$expect" "podman history $options" + done +} + +@test "podman history - json" { + tests=" +id | [0-9a-f]\\\{64\\\} +created | [0-9-]\\\+T[0-9:]\\\+\\\.[0-9]\\\+Z +size | -\\\?[0-9]\\\+ +" + + run_podman history --format json $IMAGE + + parse_table "$tests" | while read field expect; do + # HACK: we can't include '|' in the table + if [ "$field" = "id" ]; then expect="$expect\|<missing>";fi + + # output is an array of dicts; check each one + count=$(echo "$output" | jq '. | length') + i=0 + while [ $i -lt $count ]; do + actual=$(echo "$output" | jq -r ".[$i].$field") + is "$actual" "$expect\$" "jq .[$i].$field" + i=$(expr $i + 1) + done + done + +} + +# vim: filetype=sh diff --git a/test/system/200-pod-top.bats b/test/system/200-pod-top.bats new file mode 100644 index 000000000..81c4be3ff --- /dev/null +++ b/test/system/200-pod-top.bats @@ -0,0 +1,37 @@ +#!/usr/bin/env bats + +load helpers + +@test "podman pod top - containers in different PID namespaces" { + skip_if_rootless + + run_podman pod create + podid="$output" + + # Start two containers... + run_podman run -d --pod $podid $IMAGE top -d 2 + cid1="$output" + run_podman run -d --pod $podid $IMAGE top -d 2 + cid2="$output" + + # ...and wait for them to actually start. + wait_for_output "PID \+PPID \+USER " $cid1 + wait_for_output "PID \+PPID \+USER " $cid2 + + # Both containers have emitted at least one top-like line. + # Now run 'pod top', and expect two 'top -d 2' processes running. + run_podman pod top $podid + is "$output" ".*root.*top -d 2.*root.*top -d 2" "two 'top' containers" + + # There should be a /pause container + # FIXME: sometimes there is, sometimes there isn't. If anyone ever + # actually figures this out, please either reenable this line or + # remove it entirely. + #is "$output" ".*0 \+1 \+0 \+[0-9. ?s]\+/pause" "there is a /pause container" + + # Clean up + run_podman pod rm -f $podid +} + + +# vim: filetype=sh diff --git a/test/system/README.md b/test/system/README.md new file mode 100644 index 000000000..6ac408f4e --- /dev/null +++ b/test/system/README.md @@ -0,0 +1,82 @@ +Quick overview of podman system tests. The idea is to use BATS, +but with a framework for making it easy to add new tests and to +debug failures. + +Quick Start +=========== + +Look at [030-run.bats](030-run.bats) for a simple but packed example. +This introduces the basic set of helper functions: + +* `setup` (implicit) - resets container storage so there's +one and only one (standard) image, and no running containers. + +* `parse_table` - you can define tables of inputs and expected results, +then read those in a `while` loop. This makes it easy to add new tests. +Because bash is not a programming language, the caller of `parse_table` +sometimes needs to massage the returned values; `015-run.bats` offers +examples of how to deal with the more typical such issues. + +* `run_podman` - runs command defined in `$PODMAN` (default: 'podman' +but could also be './bin/podman' or 'podman-remote'), with a timeout. +Checks its exit status. + +* `is` - compare actual vs expected output. Emits a useful diagnostic +on failure. + +* `die` - output a properly-formatted message to stderr, and fail test + +* `skip_if_rootless` - if rootless, skip this test with a helpful message. + +* `random_string` - returns a pseudorandom alphanumeric string + +Test files are of the form `NNN-name.bats` where NNN is a three-digit +number. Please preserve this convention, it simplifies viewing the +directory and understanding test order. In particular, `00x` tests +should be reserved for a first-pass fail-fast subset of tests: + + bats test/system/00*.bats || exit 1 + bats test/system + +...the goal being to provide quick feedback on catastrophic failures +without having to wait for the entire test suite. + + +Analyzing test failures +======================= + +The top priority for this scheme is to make it easy to diagnose +what went wrong. To that end, `podman_run` always logs all invoked +commands, their output and exit codes. In a normal run you will never +see this, but BATS will display it on failure. The goal here is to +give you everything you need to diagnose without having to rerun tests. + +The `is` comparison function is designed to emit useful diagnostics, +in particular, the actual and expected strings. Please do not use +the horrible BATS standard of `[ x = y ]`; that's nearly useless +for tracking down failures. + +If the above are not enough to help you track down a failure: + + +Debugging tests +--------------- + +Some functions have `dprint` statements. To see the output of these, +set `PODMAN_TEST_DEBUG="funcname"` where `funcname` is the name of +the function or perhaps just a substring. + + +Requirements +============ + +The `jq` tool is needed for parsing JSON output. + + +Further Details +=============== + +TBD. For now, look in [helpers.bash](helpers.bash); each helper function +has (what are intended to be) helpful header comments. For even more +examples, see and/or run `helpers.t`; that's a regression test +and provides a thorough set of examples of how the helpers work. diff --git a/test/system/helpers.bash b/test/system/helpers.bash new file mode 100644 index 000000000..431228498 --- /dev/null +++ b/test/system/helpers.bash @@ -0,0 +1,349 @@ +# -*- bash -*- + +# Podman command to run; may be podman-remote +PODMAN=${PODMAN:-podman} + +# Standard image to use for most tests +PODMAN_TEST_IMAGE_REGISTRY=${PODMAN_TEST_IMAGE_REGISTRY:-"quay.io"} +PODMAN_TEST_IMAGE_USER=${PODMAN_TEST_IMAGE_USER:-"libpod"} +PODMAN_TEST_IMAGE_NAME=${PODMAN_TEST_IMAGE_NAME:-"alpine_labels"} +PODMAN_TEST_IMAGE_TAG=${PODMAN_TEST_IMAGE_TAG:-"latest"} +PODMAN_TEST_IMAGE_FQN="$PODMAN_TEST_IMAGE_REGISTRY/$PODMAN_TEST_IMAGE_USER/$PODMAN_TEST_IMAGE_NAME:$PODMAN_TEST_IMAGE_TAG" + +# Because who wants to spell that out each time? +IMAGE=$PODMAN_TEST_IMAGE_FQN + +# Default timeout for a podman command. +PODMAN_TIMEOUT=${PODMAN_TIMEOUT:-60} + +############################################################################### +# BEGIN setup/teardown tools + +# Provide common setup and teardown functions, but do not name them such! +# That way individual tests can override with their own setup/teardown, +# while retaining the ability to include these if they so desire. + +# Setup helper: establish a test environment with exactly the images needed +function basic_setup() { + # Clean up all containers + run_podman rm --all --force + + # Clean up all images except those desired + found_needed_image= + run_podman images --all --format '{{.Repository}}:{{.Tag}} {{.ID}}' + for line in "${lines[@]}"; do + set $line + if [ "$1" == "$PODMAN_TEST_IMAGE_FQN" ]; then + found_needed_image=1 + else + echo "# setup(): removing stray images" >&3 + run_podman rmi --force "$1" >/dev/null 2>&1 || true + run_podman rmi --force "$2" >/dev/null 2>&1 || true + fi + done + + # Make sure desired images are present + if [ -z "$found_needed_image" ]; then + run_podman pull "$PODMAN_TEST_IMAGE_FQN" + fi + + # Argh. Although BATS provides $BATS_TMPDIR, it's just /tmp! + # That's bloody worthless. Let's make our own, in which subtests + # can write whatever they like and trust that it'll be deleted + # on cleanup. + # TODO: do this outside of setup, so it carries across tests? + PODMAN_TMPDIR=$(mktemp -d --tmpdir=${BATS_TMPDIR:-/tmp} podman_bats.XXXXXX) +} + +# Basic teardown: remove all pods and containers +function basic_teardown() { + echo "# [teardown]" >&2 + run_podman '?' pod rm --all --force + run_podman '?' rm --all --force + + /bin/rm -rf $PODMAN_TMPDIR +} + + +# Provide the above as default methods. +function setup() { + basic_setup +} + +function teardown() { + basic_teardown +} + + +# Helpers useful for tests running rmi +function archive_image() { + local image=$1 + + # FIXME: refactor? + archive_basename=$(echo $1 | tr -c a-zA-Z0-9._- _) + archive=$BATS_TMPDIR/$archive_basename.tar + + run_podman save -o $archive $image +} + +function restore_image() { + local image=$1 + + archive_basename=$(echo $1 | tr -c a-zA-Z0-9._- _) + archive=$BATS_TMPDIR/$archive_basename.tar + + run_podman restore $archive +} + +# END setup/teardown tools +############################################################################### +# BEGIN podman helpers + +################ +# run_podman # Invoke $PODMAN, with timeout, using BATS 'run' +################ +# +# This is the preferred mechanism for invoking podman: first, it +# invokes $PODMAN, which may be 'podman-remote' or '/some/path/podman'. +# +# Second, we use 'timeout' to abort (with a diagnostic) if something +# takes too long; this is preferable to a CI hang. +# +# Third, we log the command run and its output. This doesn't normally +# appear in BATS output, but it will if there's an error. +# +# Next, we check exit status. Since the normal desired code is 0, +# that's the default; but the first argument can override: +# +# run_podman 125 nonexistent-subcommand +# run_podman '?' some-other-command # let our caller check status +# +# Since we use the BATS 'run' mechanism, $output and $status will be +# defined for our caller. +# +function run_podman() { + # Number as first argument = expected exit code; default 0 + expected_rc=0 + case "$1" in + [0-9]) expected_rc=$1; shift;; + [1-9][0-9]) expected_rc=$1; shift;; + [12][0-9][0-9]) expected_rc=$1; shift;; + '?') expected_rc= ; shift;; # ignore exit code + esac + + # stdout is only emitted upon error; this echo is to help a debugger + echo "\$ $PODMAN $*" + run timeout --foreground -v --kill=10 $PODMAN_TIMEOUT $PODMAN "$@" + # without "quotes", multiple lines are glommed together into one + if [ -n "$output" ]; then + echo "$output" + fi + if [ "$status" -ne 0 ]; then + echo -n "[ rc=$status "; + if [ -n "$expected_rc" ]; then + if [ "$status" -eq "$expected_rc" ]; then + echo -n "(expected) "; + else + echo -n "(** EXPECTED $expected_rc **) "; + fi + fi + echo "]" + fi + + if [ "$status" -eq 124 ]; then + if expr "$output" : ".*timeout: sending" >/dev/null; then + echo "*** TIMED OUT ***" + false + fi + fi + + if [ -n "$expected_rc" ]; then + if [ "$status" -ne "$expected_rc" ]; then + die "exit code is $status; expected $expected_rc" + fi + fi +} + + +# Wait for certain output from a container, indicating that it's ready. +function wait_for_output { + local sleep_delay=5 + local how_long=$PODMAN_TIMEOUT + local expect= + local cid= + + # Arg processing. A single-digit number is how long to sleep between + # iterations; a 2- or 3-digit number is the total time to wait; all + # else are, in order, the string to expect and the container name/ID. + local i + for i in "$@"; do + if expr "$i" : '[0-9]\+$' >/dev/null; then + if [ $i -le 9 ]; then + sleep_delay=$i + else + how_long=$i + fi + elif [ -z "$expect" ]; then + expect=$i + else + cid=$i + fi + done + + [ -n "$cid" ] || die "FATAL: wait_for_ready: no container name/ID in '$*'" + + t1=$(expr $SECONDS + $how_long) + while [ $SECONDS -lt $t1 ]; do + run_podman logs $cid + if expr "$output" : ".*$expect" >/dev/null; then + return + fi + + sleep $sleep_delay + done + + die "timed out waiting for '$expect' from $cid" +} + +# Shortcut for the lazy +function wait_for_ready { + wait_for_output 'READY' "$@" +} + +# END podman helpers +############################################################################### +# BEGIN miscellaneous tools + +###################### +# skip_if_rootless # ...with an optional message +###################### +function skip_if_rootless() { + if [ "$(id -u)" -eq 0 ]; then + return + fi + + skip "${1:-not applicable under rootless podman}" +} + + +######### +# die # Abort with helpful message +######### +function die() { + echo "#/vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv" >&2 + echo "#| FAIL: $*" >&2 + echo "#\\^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" >&2 + false +} + + +######## +# is # Compare actual vs expected string; fail w/diagnostic if mismatch +######## +# +# Compares given string against expectations, using 'expr' to allow patterns. +# +# Examples: +# +# is "$actual" "$expected" "descriptive test name" +# is "apple" "orange" "name of a test that will fail in most universes" +# is "apple" "[a-z]\+" "this time it should pass" +# +function is() { + local actual="$1" + local expect="$2" + local testname="${3:-FIXME}" + + if [ -z "$expect" ]; then + if [ -z "$actual" ]; then + return + fi + expect='[no output]' + elif expr "$actual" : "$expect" >/dev/null; then + return + fi + + # This is a multi-line message, which may in turn contain multi-line + # output, so let's format it ourself, readably + local -a actual_split + readarray -t actual_split <<<"$actual" + printf "#/vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n" >&2 + printf "#| FAIL: $testname\n" >&2 + printf "#| expected: '%s'\n" "$expect" >&2 + printf "#| actual: '%s'\n" "${actual_split[0]}" >&2 + local line + for line in "${actual_split[@]:1}"; do + printf "#| > '%s'\n" "$line" >&2 + done + printf "#\\^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n" >&2 + false +} + + +############ +# dprint # conditional debug message +############ +# +# Set PODMAN_TEST_DEBUG to the name of one or more functions you want to debug +# +# Examples: +# +# $ PODMAN_TEST_DEBUG=parse_table bats . +# $ PODMAN_TEST_DEBUG="test_podman_images test_podman_run" bats . +# +function dprint() { + test -z "$PODMAN_TEST_DEBUG" && return + + caller="${FUNCNAME[1]}" + + # PODMAN_TEST_DEBUG is a space-separated list of desired functions + # e.g. "parse_table test_podman_images" (or even just "table") + for want in $PODMAN_TEST_DEBUG; do + # Check if our calling function matches any of the desired strings + if expr "$caller" : ".*$want" >/dev/null; then + echo "# ${FUNCNAME[1]}() : $*" >&3 + return + fi + done +} + + +################# +# parse_table # Split a table on '|' delimiters; return space-separated +################# +# +# See sample .bats scripts for examples. The idea is to list a set of +# tests in a table, then use simple logic to iterate over each test. +# Columns are separated using '|' (pipe character) because sometimes +# we need spaces in our fields. +# +function parse_table() { + while read line; do + test -z "$line" && continue + + declare -a row=() + while read col; do + dprint "col=<<$col>>" + row+=("$col") + done < <(echo "$line" | tr '|' '\012' | sed -e 's/^ *//' -e 's/\\/\\\\/g') + + printf "%q " "${row[@]}" + printf "\n" + done <<<"$1" +} + + +################### +# random_string # Returns a pseudorandom human-readable string +################### +# +# Numeric argument, if present, is desired length of string +# +function random_string() { + local length=${1:-10} + + head /dev/urandom | tr -dc a-zA-Z0-9 | head -c$length +} + +# END miscellaneous tools +############################################################################### diff --git a/test/system/helpers.t b/test/system/helpers.t new file mode 100755 index 000000000..7b4e48a84 --- /dev/null +++ b/test/system/helpers.t @@ -0,0 +1,145 @@ +#!/bin/bash +# +# regression tests for helpers.bash +# +# Some of those helper functions are fragile, and we don't want to break +# anything if we have to mess with them. +# + +source $(dirname $0)/helpers.bash + +die() { + echo "$(basename $0): $*" >&2 + exit 1 +} + +# Iterator and return code; updated in check_result() +testnum=0 +rc=0 + +############################################################################### +# BEGIN test the parse_table helper + +function check_result { + testnum=$(expr $testnum + 1) + if [ "$1" = "$2" ]; then + echo "ok $testnum $3 = $1" + else + echo "not ok $testnum $3" + echo "# expected: $2" + echo "# actual: $1" + rc=1 + fi +} + +# IMPORTANT NOTE: you have to do +# this: while ... done < <(parse_table) +# and not: parse_table | while read ... +# +# ...because piping to 'while' makes it a subshell, hence testnum and rc +# will not be updated. +# +while read x y z; do + check_result "$x" "a" "parse_table simple: column 1" + check_result "$y" "b" "parse_table simple: column 2" + check_result "$z" "c" "parse_table simple: column 3" +done < <(parse_table "a | b | c") + +# More complicated example, with spaces +while read x y z; do + check_result "$x" "a b" "parse_table with spaces: column 1" + check_result "$y" "c d" "parse_table with spaces: column 2" + check_result "$z" "e f g" "parse_table with spaces: column 3" +done < <(parse_table "a b | c d | e f g") + +# Multi-row, with spaces and with blank lines +table=" +a | b | c d e +d e f | g h | i j +" +declare -A expect=( + [0,0]="a" + [0,1]="b" + [0,2]="c d e" + [1,0]="d e f" + [1,1]="g h" + [1,2]="i j" +) +row=0 +while read x y z;do + check_result "$x" "${expect[$row,0]}" "parse_table multi_row[$row,0]" + check_result "$y" "${expect[$row,1]}" "parse_table multi_row[$row,1]" + check_result "$z" "${expect[$row,2]}" "parse_table multi_row[$row,2]" + row=$(expr $row + 1) +done < <(parse_table "$table") + +# Backslash handling. The first element should have none, the second some +while read x y;do + check_result "$x" '[0-9]{2}' "backslash test - no backslashes" + check_result "$y" '[0-9]\{3\}' "backslash test - one backslash each" +done < <(parse_table "[0-9]{2} | [0-9]\\\{3\\\}") + +# Empty strings. I wish we could convert those to real empty strings. +while read x y z; do + check_result "$x" "''" "empty string - left-hand" + check_result "$y" "''" "empty string - middle" + check_result "$z" "''" "empty string - right" +done < <(parse_table " | |") + +# Quotes +while read x y z;do + check_result "$x" "a 'b c'" "single quotes" + check_result "$y" "d \"e f\" g" "double quotes" + check_result "$z" "h" "no quotes" + + # FIXME FIXME FIXME: this is the only way I can find to get bash-like + # splitting of tokens. It really should be done inside parse_table + # but I can't find any way of doing so. If you can find a way, please + # update this test and any BATS tests that rely on quoting. + eval set "$x" + check_result "$1" "a" "single quotes - token split - 1" + check_result "$2" "b c" "single quotes - token split - 2" + check_result "$3" "" "single quotes - token split - 3" + + eval set "$y" + check_result "$1" "d" "double quotes - token split - 1" + check_result "$2" "e f" "double quotes - token split - 2" + check_result "$3" "g" "double quotes - token split - 3" +done < <(parse_table "a 'b c' | d \"e f\" g | h") + +# END test the parse_table helper +############################################################################### +# BEGIN dprint + +function dprint_test_1() { + dprint "$*" +} + +# parse_table works, might as well use it +# +# <value of PODMAN_TEST_DEBUG> | <blank for no msg, - for msg> | <desc> +# +table=" + | | debug unset +dprint_test | - | substring match +dprint_test_1 | - | exact match +dprint_test_10 | | caller name mismatch +xxx yyy zzz | | multiple callers, no match +dprint_test_1 xxx yyy zzz | - | multiple callers, match at start +xxx dprint_test_1 yyy zzz | - | multiple callers, match in middle +xxx yyy zzz dprint_test_1 | - | multiple callers, match at end +" +while read var expect name; do + random_string=$(random_string 20) + PODMAN_TEST_DEBUG="$var" result=$(dprint_test_1 "$random_string" 3>&1) + expect_full="" + if [ -n "$expect" -a "$expect" != "''" ]; then + expect_full="# dprint_test_1() : $random_string" + fi + check_result "$result" "$expect_full" "DEBUG='$var' - $name" +done < <(parse_table "$table") + +# END dprint +############################################################################### + +exit $rc diff --git a/test/system/libpod_suite_test.go b/test/system/libpod_suite_test.go deleted file mode 100644 index 5de50e4e7..000000000 --- a/test/system/libpod_suite_test.go +++ /dev/null @@ -1,217 +0,0 @@ -package system - -import ( - "fmt" - "os" - "strings" - "testing" - - . "github.com/containers/libpod/test/utils" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var ( - PODMAN_BINARY string - GLOBALOPTIONS = []string{"--cgroup-manager", - "--cni-config-dir", - "--config", "-c", - "--conmon", - "--cpu-profile", - "--log-level", - "--root", - "--tmpdir", - "--runroot", - "--runtime", - "--storage-driver", - "--storage-opt", - "--syslog", - } - PODMAN_SUBCMD = []string{"attach", - "commit", - "container", - "build", - "create", - "diff", - "exec", - "export", - "history", - "image", - "images", - "import", - "info", - "inspect", - "kill", - "load", - "login", - "logout", - "logs", - "mount", - "pause", - "ps", - "pod", - "port", - "pull", - "push", - "restart", - "rm", - "rmi", - "run", - "save", - "search", - "start", - "stats", - "stop", - "tag", - "top", - "umount", - "unpause", - "version", - "wait", - "h", - } - INTEGRATION_ROOT string - ARTIFACT_DIR = "/tmp/.artifacts" - ALPINE = "docker.io/library/alpine:latest" - BB = "docker.io/library/busybox:latest" - BB_GLIBC = "docker.io/library/busybox:glibc" - fedoraMinimal = "registry.fedoraproject.org/fedora-minimal:latest" - nginx = "quay.io/baude/alpine_nginx:latest" - redis = "docker.io/library/redis:alpine" - registry = "docker.io/library/registry:2" - infra = "k8s.gcr.io/pause:3.1" - defaultWaitTimeout = 90 -) - -// PodmanTestSystem struct for command line options -type PodmanTestSystem struct { - PodmanTest - GlobalOptions map[string]string - PodmanCmdOptions map[string][]string -} - -// TestLibpod ginkgo master function -func TestLibpod(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Libpod Suite") -} - -var _ = BeforeSuite(func() { -}) - -// PodmanTestCreate creates a PodmanTestSystem instance for the tests -func PodmanTestCreate(tempDir string) *PodmanTestSystem { - var envKey string - globalOptions := make(map[string]string) - podmanCmdOptions := make(map[string][]string) - - for _, n := range GLOBALOPTIONS { - envKey = strings.Replace(strings.ToUpper(strings.Trim(n, "-")), "-", "_", -1) - if isEnvSet(envKey) { - globalOptions[n] = os.Getenv(envKey) - } - } - - for _, n := range PODMAN_SUBCMD { - envKey = strings.Replace("PODMAN_SUBCMD_OPTIONS", "SUBCMD", strings.ToUpper(n), -1) - if isEnvSet(envKey) { - podmanCmdOptions[n] = strings.Split(os.Getenv(envKey), " ") - } - } - - podmanBinary := "podman" - if os.Getenv("PODMAN_BINARY") != "" { - podmanBinary = os.Getenv("PODMAN_BINARY") - } - - p := &PodmanTestSystem{ - PodmanTest: PodmanTest{ - PodmanBinary: podmanBinary, - ArtifactPath: ARTIFACT_DIR, - TempDir: tempDir, - }, - GlobalOptions: globalOptions, - PodmanCmdOptions: podmanCmdOptions, - } - - p.PodmanMakeOptions = p.makeOptions - - return p -} - -func (p *PodmanTestSystem) Podman(args []string) *PodmanSession { - return p.PodmanBase(args) -} - -//MakeOptions assembles all the podman options -func (p *PodmanTestSystem) makeOptions(args []string) []string { - var addOptions, subArgs []string - for _, n := range GLOBALOPTIONS { - if p.GlobalOptions[n] != "" { - addOptions = append(addOptions, n, p.GlobalOptions[n]) - } - } - - if len(args) == 0 { - return addOptions - } - - subCmd := args[0] - addOptions = append(addOptions, subCmd) - if subCmd == "unmount" { - subCmd = "umount" - } - if subCmd == "help" { - subCmd = "h" - } - - if _, ok := p.PodmanCmdOptions[subCmd]; ok { - m := make(map[string]bool) - subArgs = p.PodmanCmdOptions[subCmd] - for i := 0; i < len(subArgs); i++ { - m[subArgs[i]] = true - } - for i := 1; i < len(args); i++ { - if _, ok := m[args[i]]; !ok { - subArgs = append(subArgs, args[i]) - } - } - } else { - subArgs = args[1:] - } - - addOptions = append(addOptions, subArgs...) - - return addOptions -} - -// Cleanup cleans up the temporary store -func (p *PodmanTestSystem) Cleanup() { - // Remove all containers - stopall := p.Podman([]string{"stop", "-a", "--timeout", "0"}) - stopall.WaitWithDefaultTimeout() - - session := p.Podman([]string{"rm", "-fa"}) - session.Wait(90) - // Nuke tempdir - if err := os.RemoveAll(p.TempDir); err != nil { - fmt.Printf("%q\n", err) - } -} - -// CleanupPod cleans up the temporary store -func (p *PodmanTestSystem) CleanupPod() { - // Remove all containers - session := p.Podman([]string{"pod", "rm", "-fa"}) - session.Wait(90) - // Nuke tempdir - if err := os.RemoveAll(p.TempDir); err != nil { - fmt.Printf("%q\n", err) - } -} - -// Check if the key is set in Env -func isEnvSet(key string) bool { - _, set := os.LookupEnv(key) - return set -} diff --git a/test/system/version_test.go b/test/system/version_test.go deleted file mode 100644 index ada0093b7..000000000 --- a/test/system/version_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package system - -import ( - "fmt" - "os" - "regexp" - - . "github.com/containers/libpod/test/utils" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("Podman version test", func() { - var ( - tempdir string - err error - podmanTest *PodmanTestSystem - ) - - BeforeEach(func() { - tempdir, err = CreateTempDirInTempDir() - if err != nil { - os.Exit(1) - } - podmanTest = PodmanTestCreate(tempdir) - }) - - 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("Smoking test: podman version with extra args", func() { - logc := podmanTest.Podman([]string{"version", "anything", "-", "--"}) - logc.WaitWithDefaultTimeout() - Expect(logc.ExitCode()).To(Equal(0)) - ver := logc.OutputToString() - Expect(regexp.MatchString("Version:.*?Go Version:.*?OS/Arch", ver)).To(BeTrue()) - }) - - It("Negative test: podman version with extra flag", func() { - logc := podmanTest.Podman([]string{"version", "--foo"}) - logc.WaitWithDefaultTimeout() - Expect(logc.ExitCode()).NotTo(Equal(0)) - err, _ := logc.GrepString("Incorrect Usage: flag provided but not defined: -foo") - Expect(err).To(BeTrue()) - }) - -}) diff --git a/test/test_podman_baseline.sh b/test/test_podman_baseline.sh index 664fd2b03..5c24229bb 100755 --- a/test/test_podman_baseline.sh +++ b/test/test_podman_baseline.sh @@ -19,6 +19,7 @@ ####### # See if we want to stop on errors and/or install and then remove Docker. ####### +HOST_PORT="${HOST_PORT:-8080}" showerror=0 installdocker=0 usedocker=1 @@ -78,7 +79,99 @@ echo $image ######## # Run container and display contents in /etc ######## -podman run $image ls -alF /etc +podman run --rm $image ls -alF /etc + +######## +# Test networking, bind mounting a file, stdin/stdout redirect +######## +echo "Testing networking: ..." +port_test_failed=0 +txt1="Hello, Podman" +echo "$txt1" > /tmp/hello.txt +podman run -d --name myweb -p "$HOST_PORT:80" -w /var/www -v /tmp/hello.txt:/var/www/index.txt busybox httpd -f -p 80 +echo "$txt1" | podman exec -i myweb sh -c "cat > /var/www/index2.txt" +txt2=$( podman exec myweb cat /var/www/index2.txt ) +[ "x$txt1" == "x$txt2" ] && echo "PASS1" || { echo "FAIL1"; port_test_failed=1; } +txt2=$( podman run --rm --net host busybox wget -qO - http://localhost:$HOST_PORT/index.txt ) +[ "x$txt1" == "x$txt2" ] && echo "PASS2" || { echo "FAIL2"; port_test_failed=1; } +txt2=$( podman run --rm --net host busybox wget -qO - http://localhost:$HOST_PORT/index2.txt ) +[ "x$txt1" == "x$txt2" ] && echo "PASS3" || { echo "FAIL3"; port_test_failed=1; } +# podman run --rm --net container:myweb --add-host myweb:127.0.0.1 busybox wget -qO - http://myweb/index.txt +rm /tmp/hello.txt +podman stop myweb +podman rm myweb +[ "0$port_test_failed" -eq 1 ] && [ "0$showerror" -eq 1 ] && { + echo "networking test failed"; + exit -1; +} + + +######## +# pull and run many containers in parallel, test locks ..etc. +######## +prun_test_failed=0 +podman rmi docker.io/library/busybox:latest > /dev/null || : +for i in `seq 10` +do ( podman run -d --name b$i docker.io/library/busybox:latest busybox httpd -f -p 80 )& +done +echo -e "\nwaiting for creation...\n" +wait +echo -e "\ndone\n" +# assert we have 10 running containers +count=$( podman ps -q | wc -l ) +[ "x$count" == "x10" ] && echo "PASS" || { echo "FAIL, expecting 10 found $count"; prun_test_failed=1; } +[ "0$prun_test_failed" -eq 1 ] && [ "0$showerror" -eq 1 ] && { + echo "was expecting 10 running containers"; + exit -1; +} + +prun_test_failed=0 +for i in `seq 10`; do ( podman stop -t=1 b$i; podman rm b$i )& done +echo -e "\nwaiting for deletion...\n" +wait +echo -e "\ndone\n" +# assert we have 0 running containers +count=$( podman ps -q | wc -l ) +[ "x$count" == "x0" ] && echo "PASS" || { echo "FAIL, expecting 0 found $count"; prun_test_failed=1; } +[ "0$prun_test_failed" -eq 1 ] && [ "0$showerror" -eq 1 ] && { + echo "was expecting 0 running containers"; + exit -1; +} + + + +######## +# run many containers in parallel for an existing image, test locks ..etc. +######## +prun_test_failed=0 +podman pull docker.io/library/busybox:latest > /dev/null || : +for i in `seq 10` +do ( podman run -d --name c$i docker.io/library/busybox:latest busybox httpd -f -p 80 )& +done +echo -e "\nwaiting for creation...\n" +wait +echo -e "\ndone\n" +# assert we have 10 running containers +count=$( podman ps -q | wc -l ) +[ "x$count" == "x10" ] && echo "PASS" || { echo "FAIL, expecting 10 found $count"; prun_test_failed=1; } +[ "0$prun_test_failed" -eq 1 ] && [ "0$showerror" -eq 1 ] && { + echo "was expecting 10 running containers"; + exit -1; +} + + +for i in `seq 10`; do ( podman stop -t=1 c$i; podman rm c$i )& done +echo -e "\nwaiting for deletion...\n" +wait +echo -e "\ndone\n" +# assert we have 0 running containers +count=$( podman ps -q | wc -l ) +[ "x$count" == "x0" ] && echo "PASS" || { echo "FAIL, expecting 0 found $count"; prun_test_failed=1; } +[ "0$prun_test_failed" -eq 1 ] && [ "0$showerror" -eq 1 ] && { + echo "was expecting 0 running containers"; + exit -1; +} + ######## # Run Java in the container - should ERROR but never stop 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) |