diff options
45 files changed, 1114 insertions, 194 deletions
@@ -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/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/cp.go b/cmd/podman/cp.go index 5958370b6..a3411b158 100644 --- a/cmd/podman/cp.go +++ b/cmd/podman/cp.go @@ -43,6 +43,8 @@ var ( func init() { cpCommand.Command = _cpCommand + flags := cpCommand.Flags() + flags.BoolVar(&cpCommand.Extract, "extract", false, "Extract the tar file into the destination directory.") rootCmd.AddCommand(cpCommand.Command) } @@ -61,10 +63,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 +169,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 +222,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 +243,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 +267,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..a7b9bbf31 100644 --- a/cmd/podman/create.go +++ b/cmd/podman/create.go @@ -12,6 +12,7 @@ import ( "strings" "syscall" + "github.com/containers/image/manifest" "github.com/containers/libpod/cmd/podman/cliconfig" "github.com/containers/libpod/cmd/podman/libpodruntime" "github.com/containers/libpod/cmd/podman/shared" @@ -117,6 +118,10 @@ func createInit(c *cliconfig.PodmanCommand) error { } func createContainer(c *cliconfig.PodmanCommand, runtime *libpod.Runtime) (*libpod.Container, *cc.CreateConfig, error) { + var ( + hasHealthCheck bool + healthCheck *manifest.Schema2HealthConfig + ) if c.Bool("trace") { span, _ := opentracing.StartSpanFromContext(Ctx, "createContainer") defer span.Finish() @@ -163,12 +168,32 @@ func createContainer(c *cliconfig.PodmanCommand, runtime *libpod.Runtime) (*libp } else { imageName = newImage.ID() } + + // add healthcheck if it exists AND is correct mediatype + _, mediaType, err := newImage.Manifest(ctx) + if err != nil { + return nil, nil, errors.Wrapf(err, "unable to determine mediatype of image %s", newImage.ID()) + } + if mediaType == manifest.DockerV2Schema2MediaType { + healthCheck, err = newImage.GetHealthCheck(ctx) + if err != nil { + return nil, nil, errors.Wrapf(err, "unable to get healthcheck for %s", c.InputArgs[0]) + } + if healthCheck != nil { + hasHealthCheck = true + } + } } createConfig, err := parseCreateOpts(ctx, c, runtime, imageName, data) if err != nil { return nil, nil, err } + // Because parseCreateOpts does derive anything from the image, we add health check + // at this point. The rest is done by WithOptions. + createConfig.HasHealthCheck = hasHealthCheck + createConfig.HealthCheck = healthCheck + ctr, err := createContainerFromCreateConfig(runtime, createConfig, ctx, nil) if err != nil { return nil, nil, err diff --git a/cmd/podman/healthcheck.go b/cmd/podman/healthcheck.go new file mode 100644 index 000000000..e7cc125cc --- /dev/null +++ b/cmd/podman/healthcheck.go @@ -0,0 +1,25 @@ +package main + +import ( + "github.com/containers/libpod/cmd/podman/cliconfig" + "github.com/spf13/cobra" +) + +var healthcheckDescription = "Manage health checks on containers" +var healthcheckCommand = cliconfig.PodmanCommand{ + Command: &cobra.Command{ + Use: "healthcheck", + Short: "Manage Healthcheck", + Long: healthcheckDescription, + }, +} + +// Commands that are universally implemented +var healthcheckCommands []*cobra.Command + +func init() { + healthcheckCommand.AddCommand(healthcheckCommands...) + healthcheckCommand.AddCommand(getHealtcheckSubCommands()...) + healthcheckCommand.SetUsageTemplate(UsageTemplate()) + rootCmd.AddCommand(healthcheckCommand.Command) +} diff --git a/cmd/podman/healthcheck_run.go b/cmd/podman/healthcheck_run.go new file mode 100644 index 000000000..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/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..a4f2e5e10 100644 --- a/cmd/podman/images.go +++ b/cmd/podman/images.go @@ -375,6 +375,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/main.go b/cmd/podman/main.go index b3faf05c0..97ffa8930 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, } diff --git a/cmd/podman/play_kube.go b/cmd/podman/play_kube.go index ac46ad5c6..1910b68b5 100644 --- a/cmd/podman/play_kube.go +++ b/cmd/podman/play_kube.go @@ -25,6 +25,11 @@ 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" @@ -144,12 +149,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 +228,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 +273,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..7067f2429 100644 --- a/cmd/podman/pod.go +++ b/cmd/podman/pod.go @@ -29,12 +29,13 @@ var podSubCommands = []*cobra.Command{ _podRestartCommand, _podRmCommand, _podStartCommand, + _podStatsCommand, _podStopCommand, + _podTopCommand, _podUnpauseCommand, } func init() { podCommand.AddCommand(podSubCommands...) - podCommand.AddCommand(getPodSubCommands()...) podCommand.SetUsageTemplate(UsageTemplate()) } diff --git a/cmd/podman/pod_stats.go b/cmd/podman/pod_stats.go index f5edd21f8..2761ce9cc 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" @@ -51,10 +51,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 +72,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 +83,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 +99,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 +107,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 +143,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 +153,7 @@ func podStatsCmd(c *cliconfig.PodStatsValues) error { if err != nil { return err } - newPod := libpod.PodContainerStats{ + newPod := adapter.PodContainerStats{ Pod: p, ContainerStats: newPodStats, } @@ -202,7 +181,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 +190,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 +274,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 +283,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_top.go b/cmd/podman/pod_top.go index 6a26e3dff..c5383d376 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" @@ -50,8 +49,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 +67,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/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/wait.go b/cmd/podman/wait.go index 9df2e3208..6c2a8c9ff 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" ) @@ -49,43 +49,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/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/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/healthcheck.go b/libpod/healthcheck.go new file mode 100644 index 000000000..81addb9a8 --- /dev/null +++ b/libpod/healthcheck.go @@ -0,0 +1,92 @@ +package libpod + +import ( + "os" + "strings" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// HealthCheckStatus represents the current state of a container +type HealthCheckStatus int + +const ( + // HealthCheckSuccess means the health worked + HealthCheckSuccess HealthCheckStatus = iota + // HealthCheckFailure means the health ran and failed + HealthCheckFailure HealthCheckStatus = iota + // HealthCheckContainerStopped means the health check cannot + // be run because the container is stopped + HealthCheckContainerStopped HealthCheckStatus = iota + // HealthCheckContainerNotFound means the container could + // not be found in local store + HealthCheckContainerNotFound HealthCheckStatus = iota + // HealthCheckNotDefined means the container has no health + // check defined in it + HealthCheckNotDefined HealthCheckStatus = iota + // HealthCheckInternalError means somes something failed obtaining or running + // a given health check + HealthCheckInternalError HealthCheckStatus = iota + // HealthCheckDefined means the healthcheck was found on the container + HealthCheckDefined HealthCheckStatus = iota +) + +// HealthCheck verifies the state and validity of the healthcheck configuration +// on the container and then executes the healthcheck +func (r *Runtime) HealthCheck(name string) (HealthCheckStatus, error) { + container, err := r.LookupContainer(name) + if err != nil { + return HealthCheckContainerNotFound, errors.Wrapf(err, "unable to lookup %s to perform a health check", name) + } + hcStatus, err := checkHealthCheckCanBeRun(container) + if err == nil { + return container.RunHealthCheck() + } + return hcStatus, err +} + +// RunHealthCheck runs the health check as defined by the container +func (c *Container) RunHealthCheck() (HealthCheckStatus, error) { + var newCommand []string + hcStatus, err := checkHealthCheckCanBeRun(c) + if err != nil { + return hcStatus, err + } + hcCommand := c.HealthCheckConfig().Test + if len(hcCommand) > 0 && hcCommand[0] == "CMD-SHELL" { + newCommand = []string{"sh", "-c"} + newCommand = append(newCommand, hcCommand[1:]...) + } else { + newCommand = hcCommand + } + // TODO when history/logging is implemented for healthcheck, we need to change the output streams + // so we can capture i/o + streams := new(AttachStreams) + streams.OutputStream = os.Stdout + streams.ErrorStream = os.Stderr + streams.InputStream = os.Stdin + streams.AttachOutput = true + streams.AttachError = true + streams.AttachInput = true + + logrus.Debugf("executing health check command %s for %s", strings.Join(newCommand, " "), c.ID()) + if err := c.Exec(false, false, []string{}, newCommand, "", "", streams, 0); err != nil { + return HealthCheckFailure, err + } + return HealthCheckSuccess, nil +} + +func checkHealthCheckCanBeRun(c *Container) (HealthCheckStatus, error) { + cstate, err := c.State() + if err != nil { + return HealthCheckInternalError, err + } + if cstate != ContainerStateRunning { + return HealthCheckContainerStopped, errors.Errorf("container %s is not running", c.ID()) + } + if !c.HasHealthCheck() { + return HealthCheckNotDefined, errors.Errorf("container %s has no defined healthcheck", c.ID()) + } + return HealthCheckDefined, nil +} diff --git a/libpod/image/image.go b/libpod/image/image.go index b20419d7b..8c98de3d3 100644 --- a/libpod/image/image.go +++ b/libpod/image/image.go @@ -1151,3 +1151,32 @@ func (i *Image) Save(ctx context.Context, source, format, output string, moreTag return nil } + +// GetConfigBlob returns a schema2image. If the image is not a schema2, then +// it will return an error +func (i *Image) GetConfigBlob(ctx context.Context) (*manifest.Schema2Image, error) { + imageRef, err := i.toImageRef(ctx) + if err != nil { + return nil, err + } + b, err := imageRef.ConfigBlob(ctx) + if err != nil { + return nil, errors.Wrapf(err, "unable to get config blob for %s", i.ID()) + } + blob := manifest.Schema2Image{} + if err := json.Unmarshal(b, &blob); err != nil { + return nil, errors.Wrapf(err, "unable to parse image blob for %s", i.ID()) + } + return &blob, nil + +} + +// GetHealthCheck returns a HealthConfig for an image. This function only works with +// schema2 images. +func (i *Image) GetHealthCheck(ctx context.Context) (*manifest.Schema2HealthConfig, error) { + configBlob, err := i.GetConfigBlob(ctx) + if err != nil { + return nil, err + } + return configBlob.ContainerConfig.Healthcheck, nil +} diff --git a/libpod/options.go b/libpod/options.go index 1e8592a25..5ad2824d9 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -7,6 +7,7 @@ import ( "regexp" "syscall" + "github.com/containers/image/manifest" "github.com/containers/libpod/pkg/namespaces" "github.com/containers/storage" "github.com/containers/storage/pkg/idtools" @@ -1469,3 +1470,14 @@ func WithInfraContainerPorts(bindings []ocicni.PortMapping) PodCreateOption { return nil } } + +// WithHealthCheck adds the healthcheck to the container config +func WithHealthCheck(healthCheck *manifest.Schema2HealthConfig) CtrCreateOption { + return func(ctr *Container) error { + if ctr.valid { + return ErrCtrFinalized + } + ctr.config.HealthCheckConfig = healthCheck + return nil + } +} diff --git a/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 fcce9bb86..756369196 100644 --- a/pkg/adapter/containers.go +++ b/pkg/adapter/containers.go @@ -4,7 +4,9 @@ package adapter import ( "context" + "strconv" "syscall" + "time" "github.com/containers/libpod/cmd/podman/cliconfig" "github.com/containers/libpod/libpod" @@ -103,3 +105,25 @@ func (r *LocalRuntime) KillContainers(ctx context.Context, cli *cliconfig.KillVa } 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 45926ccf9..5646d2297 100644 --- a/pkg/adapter/containers_remote.go +++ b/pkg/adapter/containers_remote.go @@ -6,7 +6,9 @@ import ( "context" "encoding/json" "errors" + "strconv" "syscall" + "time" "github.com/containers/libpod/cmd/podman/cliconfig" "github.com/containers/libpod/cmd/podman/shared" @@ -173,6 +175,30 @@ func (r *LocalRuntime) KillContainers(ctx context.Context, cli *cliconfig.KillVa 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/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/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) |