diff options
55 files changed, 721 insertions, 127 deletions
diff --git a/.gitignore b/.gitignore index d5d1206b5..e60b8c03a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,32 +1,33 @@ /.artifacts/ -/_output/ +/bin/ /brew +/build/ +/cmd/podman/varlink/iopodman.go +/cmd/podman/varlink/ioprojectatomicpodman.go /conmon/ +contrib/spec/podman.spec +*.coverprofile /docs/*.[158] /docs/*.[158].gz -/docs/remote /docs/build/ +/docs/remote +.gopathok +.idea* +.nfs* *.o *.orig +/_output/ /pause/pause.o -/bin/ +pkg/api/swagger.yaml +/pkg/varlink/iopodman.go +podman-remote*.zip +podman*.tar.gz +__pycache__ +release.txt +.ropeproject +*.rpm /test/bin2img/bin2img /test/checkseccomp/checkseccomp /test/copyimg/copyimg /test/goecho/goecho -/build/ -.nfs* -.ropeproject -__pycache__ -/cmd/podman/varlink/ioprojectatomicpodman.go -/cmd/podman/varlink/iopodman.go -/pkg/varlink/iopodman.go -.gopathok -release.txt -podman-remote*.zip -podman*.tar.gz -.idea* .vscode* -contrib/spec/podman.spec -*.rpm -*.coverprofile diff --git a/cmd/podman/common/specgen.go b/cmd/podman/common/specgen.go index 488843f41..ba9022aff 100644 --- a/cmd/podman/common/specgen.go +++ b/cmd/podman/common/specgen.go @@ -203,10 +203,17 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string s.User = c.User inputCommand := args[1:] if len(c.HealthCmd) > 0 { + if c.NoHealthCheck { + return errors.New("Cannot specify both --no-healthcheck and --health-cmd") + } s.HealthConfig, err = makeHealthCheckFromCli(c.HealthCmd, c.HealthInterval, c.HealthRetries, c.HealthTimeout, c.HealthStartPeriod) if err != nil { return err } + } else if c.NoHealthCheck { + s.HealthConfig = &manifest.Schema2HealthConfig{ + Test: []string{"NONE"}, + } } userNS := ns.UsernsMode(c.UserNS) @@ -397,6 +404,7 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string s.DNSOptions = c.Net.DNSOptions s.StaticIP = c.Net.StaticIP s.StaticMAC = c.Net.StaticMAC + s.UseImageHosts = c.Net.NoHosts // deferred, must be added on libpod side //var ImageVolumes map[string]struct{} @@ -623,10 +631,15 @@ func makeHealthCheckFromCli(inCmd, interval string, retries uint, timeout, start // first try to parse option value as JSON array of strings... cmd := []string{} - err := json.Unmarshal([]byte(inCmd), &cmd) - if err != nil { - // ...otherwise pass it to "/bin/sh -c" inside the container - cmd = []string{"CMD-SHELL", inCmd} + + if inCmd == "none" { + cmd = []string{"NONE"} + } else { + err := json.Unmarshal([]byte(inCmd), &cmd) + if err != nil { + // ...otherwise pass it to "/bin/sh -c" inside the container + cmd = []string{"CMD-SHELL", inCmd} + } } hc := manifest.Schema2HealthConfig{ Test: cmd, diff --git a/cmd/podman/containers/ps.go b/cmd/podman/containers/ps.go index 49e77abd2..a006e918d 100644 --- a/cmd/podman/containers/ps.go +++ b/cmd/podman/containers/ps.go @@ -223,7 +223,7 @@ func createPsOut() (string, string) { } headers := defaultHeaders row += "{{.ID}}" - row += "\t{{.Image}}\t{{.Command}}\t{{.CreatedHuman}}\t{{.State}}\t{{.Ports}}\t{{.Names}}" + row += "\t{{.Image}}\t{{.Command}}\t{{.CreatedHuman}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}" if listOpts.Pod { headers += "\tPOD ID\tPODNAME" @@ -282,6 +282,11 @@ func (l psReporter) State() string { return state } +// Status is a synonym for State() +func (l psReporter) Status() string { + return l.State() +} + // Command returns the container command in string format func (l psReporter) Command() string { return strings.Join(l.ListContainer.Command, " ") diff --git a/cmd/podman/containers/start.go b/cmd/podman/containers/start.go index 73f37e51f..381bf8e26 100644 --- a/cmd/podman/containers/start.go +++ b/cmd/podman/containers/start.go @@ -20,7 +20,6 @@ var ( Short: "Start one or more containers", Long: startDescription, RunE: start, - Args: cobra.MinimumNArgs(1), Example: `podman start --latest podman start 860a4b231279 5421ab43b45 podman start --interactive --attach imageID`, @@ -72,6 +71,9 @@ func init() { func start(cmd *cobra.Command, args []string) error { var errs utils.OutputErrors + if len(args) == 0 && !startOptions.Latest { + return errors.New("start requires at least one argument") + } if len(args) > 1 && startOptions.Attach { return errors.Errorf("you cannot start and attach multiple containers at once") } diff --git a/cmd/podman/manifest/add.go b/cmd/podman/manifest/add.go index 20251ca87..c83beff7a 100644 --- a/cmd/podman/manifest/add.go +++ b/cmd/podman/manifest/add.go @@ -34,6 +34,7 @@ func init() { flags.StringSliceVar(&manifestAddOpts.Annotation, "annotation", nil, "set an `annotation` for the specified image") flags.StringVar(&manifestAddOpts.Arch, "arch", "", "override the `architecture` of the specified image") flags.StringSliceVar(&manifestAddOpts.Features, "features", nil, "override the `features` of the specified image") + flags.StringVar(&manifestAddOpts.OS, "os", "", "override the `OS` of the specified image") flags.StringVar(&manifestAddOpts.OSVersion, "os-version", "", "override the OS `version` of the specified image") flags.StringVar(&manifestAddOpts.Variant, "variant", "", "override the `Variant` of the specified image") } diff --git a/cmd/podman/pods/ps.go b/cmd/podman/pods/ps.go index 808980eff..6d0d9cf7f 100644 --- a/cmd/podman/pods/ps.go +++ b/cmd/podman/pods/ps.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os" + "sort" "strings" "text/tabwriter" "text/template" @@ -32,7 +33,7 @@ var ( var ( defaultHeaders string = "POD ID\tNAME\tSTATUS\tCREATED" - inputFilters string + inputFilters []string noTrunc bool psInput entities.PodPSOptions ) @@ -48,7 +49,7 @@ func init() { flags.BoolVar(&psInput.CtrIds, "ctr-ids", false, "Display the container UUIDs. If no-trunc is not set they will be truncated") flags.BoolVar(&psInput.CtrStatus, "ctr-status", false, "Display the container status") // TODO should we make this a [] ? - flags.StringVarP(&inputFilters, "filter", "f", "", "Filter output based on conditions given") + flags.StringSliceVarP(&inputFilters, "filter", "f", []string{}, "Filter output based on conditions given") flags.StringVar(&psInput.Format, "format", "", "Pretty-print pods to JSON or using a Go template") flags.BoolVarP(&psInput.Latest, "latest", "l", false, "Act on the latest pod podman is aware of") flags.BoolVar(&psInput.Namespace, "namespace", false, "Display namespace information of the pod") @@ -67,8 +68,13 @@ func pods(cmd *cobra.Command, args []string) error { row string lpr []ListPodReporter ) + + if psInput.Quiet && len(psInput.Format) > 0 { + return errors.New("quiet and format cannot be used together") + } if cmd.Flag("filter").Changed { - for _, f := range strings.Split(inputFilters, ",") { + psInput.Filters = make(map[string][]string) + for _, f := range inputFilters { split := strings.Split(f, "=") if len(split) < 2 { return errors.Errorf("filter input must be in the form of filter=value: %s is invalid", f) @@ -81,6 +87,10 @@ func pods(cmd *cobra.Command, args []string) error { return err } + if err := sortPodPsOutput(psInput.Sort, responses); err != nil { + return err + } + if psInput.Format == "json" { b, err := json.MarshalIndent(responses, "", " ") if err != nil { @@ -95,11 +105,7 @@ func pods(cmd *cobra.Command, args []string) error { } headers, row := createPodPsOut() if psInput.Quiet { - if noTrunc { - row = "{{.Id}}\n" - } else { - row = "{{slice .Id 0 12}}\n" - } + row = "{{.Id}}\n" } if cmd.Flag("format").Changed { row = psInput.Format @@ -130,11 +136,7 @@ func pods(cmd *cobra.Command, args []string) error { func createPodPsOut() (string, string) { var row string headers := defaultHeaders - if noTrunc { - row += "{{.Id}}" - } else { - row += "{{slice .Id 0 12}}" - } + row += "{{.Id}}" row += "\t{{.Name}}\t{{.Status}}\t{{.Created}}" @@ -160,11 +162,7 @@ func createPodPsOut() (string, string) { } headers += "\tINFRA ID\n" - if noTrunc { - row += "\t{{.InfraId}}\n" - } else { - row += "\t{{slice .InfraId 0 12}}\n" - } + row += "\t{{.InfraId}}\n" return headers, row } @@ -184,6 +182,19 @@ func (l ListPodReporter) NumberOfContainers() int { return len(l.Containers) } +// ID is a wrapper to Id for compat, typos +func (l ListPodReporter) ID() string { + return l.Id() +} + +// Id returns the Pod id +func (l ListPodReporter) Id() string { + if noTrunc { + return l.ListPodsReport.Id + } + return l.ListPodsReport.Id[0:12] +} + // Added for backwards compatibility with podmanv1 func (l ListPodReporter) InfraID() string { return l.InfraId() @@ -192,6 +203,9 @@ func (l ListPodReporter) InfraID() string { // InfraId returns the infra container id for the pod // depending on trunc func (l ListPodReporter) InfraId() string { + if len(l.ListPodsReport.InfraId) == 0 { + return "" + } if noTrunc { return l.ListPodsReport.InfraId } @@ -225,3 +239,52 @@ func (l ListPodReporter) ContainerStatuses() string { } return strings.Join(statuses, ",") } + +func sortPodPsOutput(sortBy string, lprs []*entities.ListPodsReport) error { + switch sortBy { + case "created": + sort.Sort(podPsSortedCreated{lprs}) + case "id": + sort.Sort(podPsSortedId{lprs}) + case "name": + sort.Sort(podPsSortedName{lprs}) + case "number": + sort.Sort(podPsSortedNumber{lprs}) + case "status": + sort.Sort(podPsSortedStatus{lprs}) + default: + return errors.Errorf("invalid option for --sort, options are: id, names, or number") + } + return nil +} + +type lprSort []*entities.ListPodsReport + +func (a lprSort) Len() int { return len(a) } +func (a lprSort) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +type podPsSortedCreated struct{ lprSort } + +func (a podPsSortedCreated) Less(i, j int) bool { + return a.lprSort[i].Created.After(a.lprSort[j].Created) +} + +type podPsSortedId struct{ lprSort } + +func (a podPsSortedId) Less(i, j int) bool { return a.lprSort[i].Id < a.lprSort[j].Id } + +type podPsSortedNumber struct{ lprSort } + +func (a podPsSortedNumber) Less(i, j int) bool { + return len(a.lprSort[i].Containers) < len(a.lprSort[j].Containers) +} + +type podPsSortedName struct{ lprSort } + +func (a podPsSortedName) Less(i, j int) bool { return a.lprSort[i].Name < a.lprSort[j].Name } + +type podPsSortedStatus struct{ lprSort } + +func (a podPsSortedStatus) Less(i, j int) bool { + return a.lprSort[i].Status < a.lprSort[j].Status +} diff --git a/cmd/podman/pods/stats.go b/cmd/podman/pods/stats.go new file mode 100644 index 000000000..7c3597d9a --- /dev/null +++ b/cmd/podman/pods/stats.go @@ -0,0 +1,189 @@ +package pods + +import ( + "context" + "fmt" + "os" + "reflect" + "strings" + "text/tabwriter" + "text/template" + "time" + + "github.com/buger/goterm" + "github.com/containers/buildah/pkg/formats" + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/util/camelcase" + "github.com/spf13/cobra" +) + +type podStatsOptionsWrapper struct { + entities.PodStatsOptions + + // Format - pretty-print to JSON or a go template. + Format string + // NoReset - do not reset the screen when streaming. + NoReset bool + // NoStream - do not stream stats but write them once. + NoStream bool +} + +var ( + statsOptions = podStatsOptionsWrapper{} + statsDescription = `Display the containers' resource-usage statistics of one or more running pod` + // Command: podman pod _pod_ + statsCmd = &cobra.Command{ + Use: "stats [flags] [POD...]", + Short: "Display resource-usage statistics of pods", + Long: statsDescription, + RunE: stats, + Example: `podman pod stats + podman pod stats a69b23034235 named-pod + podman pod stats --latest + podman pod stats --all`, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: statsCmd, + Parent: podCmd, + }) + + flags := statsCmd.Flags() + flags.BoolVarP(&statsOptions.All, "all", "a", false, "Provide stats for all pods") + flags.StringVar(&statsOptions.Format, "format", "", "Pretty-print container statistics to JSON or using a Go template") + flags.BoolVarP(&statsOptions.Latest, "latest", "l", false, "Provide stats on the latest pod Podman is aware of") + flags.BoolVar(&statsOptions.NoReset, "no-reset", false, "Disable resetting the screen when streaming") + flags.BoolVar(&statsOptions.NoStream, "no-stream", false, "Disable streaming stats and only pull the first result") + + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + } +} + +func stats(cmd *cobra.Command, args []string) error { + // Validate input. + if err := entities.ValidatePodStatsOptions(args, &statsOptions.PodStatsOptions); err != nil { + return err + } + + format := statsOptions.Format + doJson := strings.ToLower(format) == formats.JSONString + header := getPodStatsHeader(format) + + for { + reports, err := registry.ContainerEngine().PodStats(context.Background(), args, statsOptions.PodStatsOptions) + if err != nil { + return err + } + // Print the stats in the requested format and configuration. + if doJson { + if err := printJSONPodStats(reports); err != nil { + return err + } + } else { + if !statsOptions.NoReset { + goterm.Clear() + goterm.MoveCursor(1, 1) + goterm.Flush() + } + if len(format) == 0 { + printPodStatsLines(reports) + } else if err := printFormattedPodStatsLines(format, reports, header); err != nil { + return err + } + } + if statsOptions.NoStream { + break + } + time.Sleep(time.Second) + } + + return nil +} + +func printJSONPodStats(stats []*entities.PodStatsReport) error { + b, err := json.MarshalIndent(&stats, "", " ") + if err != nil { + return err + } + fmt.Fprintf(os.Stdout, "%s\n", string(b)) + return nil +} + +func printPodStatsLines(stats []*entities.PodStatsReport) { + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + outFormat := "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" + fmt.Fprintf(w, outFormat, "POD", "CID", "NAME", "CPU %", "MEM USAGE/ LIMIT", "MEM %", "NET IO", "BLOCK IO", "PIDS") + for _, i := range stats { + if len(stats) == 0 { + fmt.Fprintf(w, outFormat, i.Pod, "--", "--", "--", "--", "--", "--", "--", "--") + } else { + fmt.Fprintf(w, outFormat, i.Pod, i.CID, i.Name, i.CPU, i.MemUsage, i.Mem, i.NetIO, i.BlockIO, i.PIDS) + } + } + w.Flush() +} + +func printFormattedPodStatsLines(format string, stats []*entities.PodStatsReport, headerNames map[string]string) error { + if len(stats) == 0 { + return nil + } + + // Use a tabwriter to align column format + w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) + // Spit out the header if "table" is present in the format + if strings.HasPrefix(format, "table") { + hformat := strings.Replace(strings.TrimSpace(format[5:]), " ", "\t", -1) + format = hformat + headerTmpl, err := template.New("header").Parse(hformat) + if err != nil { + return err + } + if err := headerTmpl.Execute(w, headerNames); err != nil { + return err + } + fmt.Fprintln(w, "") + } + + // Spit out the data rows now + dataTmpl, err := template.New("data").Parse(format) + if err != nil { + return err + } + for _, s := range stats { + if err := dataTmpl.Execute(w, s); err != nil { + return err + } + fmt.Fprintln(w, "") + } + // Flush the writer + return w.Flush() + +} + +// getPodStatsHeader returns the stats header for the specified options. +func getPodStatsHeader(format string) map[string]string { + headerNames := make(map[string]string) + if format == "" { + return headerNames + } + // Make a map of the field names for the headers + v := reflect.ValueOf(entities.PodStatsReport{}) + t := v.Type() + for i := 0; i < t.NumField(); i++ { + split := camelcase.Split(t.Field(i).Name) + value := strings.ToUpper(strings.Join(split, " ")) + switch value { + case "CPU", "MEM": + value += " %" + case "MEM USAGE": + value = "MEM USAGE / LIMIT" + } + headerNames[t.Field(i).Name] = value + } + return headerNames +} diff --git a/completions/bash/podman b/completions/bash/podman index 41a76a967..d6e9408c6 100644 --- a/completions/bash/podman +++ b/completions/bash/podman @@ -1760,6 +1760,7 @@ _podman_manifest_add() { --annotation --arch --features + --os --os-version --variant " diff --git a/docs/source/markdown/podman-manifest-add.1.md b/docs/source/markdown/podman-manifest-add.1.md index 4ecf03900..857a98e12 100644 --- a/docs/source/markdown/podman-manifest-add.1.md +++ b/docs/source/markdown/podman-manifest-add.1.md @@ -38,6 +38,13 @@ retrieved from the image's configuration information. Specify the features list which the list or index records as requirements for the image. This option is rarely used. +**--os** + +Override the OS which the list or index records as a requirement for the image. +If *imagename* refers to a manifest list or image index, the OS information +will be retrieved from it. Otherwise, it will be retrieved from the image's +configuration information. + **--os-version** Specify the OS version which the list or index records as a requirement for the diff --git a/docs/source/markdown/podman-pod-stats.1.md b/docs/source/markdown/podman-pod-stats.1.md index 962edbda0..f70a5a919 100644 --- a/docs/source/markdown/podman-pod-stats.1.md +++ b/docs/source/markdown/podman-pod-stats.1.md @@ -7,7 +7,7 @@ podman\-pod\-stats - Display a live stream of resource usage stats for container **podman pod stats** [*options*] [*pod*] ## DESCRIPTION -Display a live stream of containers in one or more pods resource usage statistics +Display a live stream of containers in one or more pods resource usage statistics. Running rootless is only supported on cgroups v2. ## OPTIONS diff --git a/docs/source/markdown/podman-pull.1.md b/docs/source/markdown/podman-pull.1.md index b3e35c672..aa558526a 100644 --- a/docs/source/markdown/podman-pull.1.md +++ b/docs/source/markdown/podman-pull.1.md @@ -4,9 +4,13 @@ podman\-pull - Pull an image from a registry ## SYNOPSIS -**podman pull** [*options*] *name*[:*tag*|@*digest*] +**podman pull** [*options*] *source* -**podman image pull** [*options*] *name*[:*tag*|@*digest*] +**podman image pull** [*options*] *source* + +**podman pull** [*options*] [*transport*]*name*[:*tag*|@*digest*] + +**podman image pull** [*options*] [*transport*]*name*[:*tag*|@*digest*] ## DESCRIPTION Copies an image from a registry onto the local machine. **podman pull** pulls an @@ -17,12 +21,12 @@ print the full image ID. **podman pull** can also pull an image using its digest **podman pull** *image*@*digest*. **podman pull** can be used to pull images from archives and local storage using different transports. -## imageID -Image stored in local container/storage +## Image storage +Images are stored in local image storage. ## SOURCE - The SOURCE is a location to get container images + The SOURCE is the location from which the container images are pulled. The Image "SOURCE" uses a "transport":"details" format. Multiple transports are supported: diff --git a/docs/source/markdown/podman-push.1.md b/docs/source/markdown/podman-push.1.md index 3f0350bcd..f029c8db1 100644 --- a/docs/source/markdown/podman-push.1.md +++ b/docs/source/markdown/podman-push.1.md @@ -14,8 +14,8 @@ Push is mainly used to push images to registries, however **podman push** can be used to save images to tarballs and directories using the following transports: **dir:**, **docker-archive:**, **docker-daemon:** and **oci-archive:**. -## imageID -Image stored in local container/storage +## Image storage +Images are pushed from those stored in local image storage. ## DESTINATION @@ -45,7 +45,7 @@ require ( github.com/opentracing/opentracing-go v1.1.0 github.com/pkg/errors v0.9.1 github.com/pmezard/go-difflib v1.0.0 - github.com/rootless-containers/rootlesskit v0.9.3 + github.com/rootless-containers/rootlesskit v0.9.4 github.com/seccomp/containers-golang v0.0.0-20190312124753-8ca8945ccf5f github.com/sirupsen/logrus v1.5.0 github.com/spf13/cobra v0.0.7 @@ -373,8 +373,8 @@ github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDa github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rootless-containers/rootlesskit v0.9.3 h1:hrkZzBZT5vEnhAso6H1jHAcc4DT8h6/hp2z4yL0xu/8= -github.com/rootless-containers/rootlesskit v0.9.3/go.mod h1:fx5DhInDgnR0Upj+2cOVacKuZJYSNKV5P/bCwGa+quQ= +github.com/rootless-containers/rootlesskit v0.9.4 h1:6ogX7l3r3nlS7eTB8ePbLSQ6TZR1aVQzRjTy2SIBOzk= +github.com/rootless-containers/rootlesskit v0.9.4/go.mod h1:fx5DhInDgnR0Upj+2cOVacKuZJYSNKV5P/bCwGa+quQ= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8 h1:2c1EFnZHIPCW8qKWgHMH/fX2PkSabFc5mrVzfUNdg5U= github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= diff --git a/libpod/image/manifests.go b/libpod/image/manifests.go index 9dbeb4cc5..7ca17f86c 100644 --- a/libpod/image/manifests.go +++ b/libpod/image/manifests.go @@ -19,6 +19,7 @@ type ManifestAddOpts struct { Arch string `json:"arch"` Features []string `json:"features"` Images []string `json:"images"` + OS string `json:"os"` OSVersion string `json:"os_version"` Variant string `json:"variant"` } @@ -86,6 +87,11 @@ func addManifestToList(ref types.ImageReference, list manifests.List, systemCont if err != nil { return nil, err } + if opts.OS != "" { + if err := list.SetOS(d, opts.OS); err != nil { + return nil, err + } + } if len(opts.OSVersion) > 0 { if err := list.SetOSVersion(d, opts.OSVersion); err != nil { return nil, err diff --git a/libpod/pod.go b/libpod/pod.go index 4cdeb1033..b5a14c165 100644 --- a/libpod/pod.go +++ b/libpod/pod.go @@ -76,27 +76,6 @@ type podState struct { InfraContainerID string } -// PodInspect represents the data we want to display for -// podman pod inspect -type PodInspect struct { - Config *PodConfig - State *PodInspectState - Containers []PodContainerInfo -} - -// PodInspectState contains inspect data on the pod's state -type PodInspectState struct { - CgroupPath string `json:"cgroupPath"` - InfraContainerID string `json:"infraContainerID"` - Status string `json:"status"` -} - -// PodContainerInfo keeps information on a container in a pod -type PodContainerInfo struct { - ID string `json:"id"` - State string `json:"state"` -} - // InfraContainerConfig is the configuration for the pod's infra container type InfraContainerConfig struct { HasInfraContainer bool `json:"makeInfraContainer"` diff --git a/pkg/api/handlers/libpod/pods.go b/pkg/api/handlers/libpod/pods.go index 618d48ac0..0b15ab0d6 100644 --- a/pkg/api/handlers/libpod/pods.go +++ b/pkg/api/handlers/libpod/pods.go @@ -11,6 +11,7 @@ import ( "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/domain/infra/abi" "github.com/containers/libpod/pkg/specgen" "github.com/containers/libpod/pkg/specgen/generate" "github.com/containers/libpod/pkg/util" @@ -419,3 +420,44 @@ func PodExists(w http.ResponseWriter, r *http.Request) { } utils.WriteResponse(w, http.StatusNoContent, "") } + +func PodStats(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + + query := struct { + NamesOrIDs []string `schema:"namesOrIDs"` + All bool `schema:"all"` + }{ + // default would go here + } + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + + // Validate input. + options := entities.PodStatsOptions{All: query.All} + if err := entities.ValidatePodStatsOptions(query.NamesOrIDs, &options); err != nil { + utils.InternalServerError(w, err) + } + + // Collect the stats and send them over the wire. + containerEngine := abi.ContainerEngine{Libpod: runtime} + reports, err := containerEngine.PodStats(r.Context(), query.NamesOrIDs, options) + + // Error checks as documented in swagger. + switch errors.Cause(err) { + case define.ErrNoSuchPod: + utils.Error(w, "one or more pods not found", http.StatusNotFound, err) + return + case nil: + // Nothing to do. + default: + utils.InternalServerError(w, err) + return + } + + utils.WriteResponse(w, http.StatusOK, reports) +} diff --git a/pkg/api/handlers/swagger/swagger.go b/pkg/api/handlers/swagger/swagger.go index 87891d4a8..0aceaf5f6 100644 --- a/pkg/api/handlers/swagger/swagger.go +++ b/pkg/api/handlers/swagger/swagger.go @@ -122,6 +122,13 @@ type swagPodTopResponse struct { } } +// List processes in pod +// swagger:response DocsPodStatsResponse +type swagPodStatsResponse struct { + // in:body + Body []*entities.PodStatsReport +} + // Inspect container // swagger:response LibpodInspectContainerResponse type swagLibpodInspectContainerResponse struct { @@ -143,7 +150,7 @@ type swagListPodsResponse struct { type swagInspectPodResponse struct { // in:body Body struct { - libpod.PodInspect + define.InspectPodData } } diff --git a/pkg/api/handlers/utils/errors.go b/pkg/api/handlers/utils/errors.go index aafc64353..3253a9be3 100644 --- a/pkg/api/handlers/utils/errors.go +++ b/pkg/api/handlers/utils/errors.go @@ -14,6 +14,9 @@ var ( ErrLinkNotSupport = errors.New("Link is not supported") ) +// TODO: document the exported functions in this file and make them more +// generic (e.g., not tied to one ctr/pod). + // Error formats an API response to an error // // apiMessage and code must match the container API, and are sent to client diff --git a/pkg/api/server/register_pods.go b/pkg/api/server/register_pods.go index 63060af41..4156dd86b 100644 --- a/pkg/api/server/register_pods.go +++ b/pkg/api/server/register_pods.go @@ -286,9 +286,36 @@ func (s *APIServer) registerPodsHandlers(r *mux.Router) error { // 200: // $ref: "#/responses/DocsPodTopResponse" // 404: - // $ref: "#/responses/NoSuchContainer" + // $ref: "#/responses/NoSuchPod" // 500: // $ref: "#/responses/InternalError" r.Handle(VersionedPath("/libpod/pods/{name}/top"), s.APIHandler(libpod.PodTop)).Methods(http.MethodGet) + // swagger:operation GET /libpod/pods/stats pods statsPod + // --- + // tags: + // - pods + // summary: Get stats for one or more pods + // description: Display a live stream of resource usage statistics for the containers in one or more pods + // parameters: + // - in: query + // name: all + // description: Provide statistics for all running pods. + // type: boolean + // - in: query + // name: namesOrIDs + // description: Names or IDs of pods. + // type: array + // items: + // type: string + // produces: + // - application/json + // responses: + // 200: + // $ref: "#/responses/DocsPodTopResponse" + // 404: + // $ref: "#/responses/NoSuchPod" + // 500: + // $ref: "#/responses/InternalError" + r.Handle(VersionedPath("/libpod/pods/stats"), s.APIHandler(libpod.PodStats)).Methods(http.MethodGet) return nil } diff --git a/pkg/bindings/pods/pods.go b/pkg/bindings/pods/pods.go index 3c60fa2a0..b213c8c73 100644 --- a/pkg/bindings/pods/pods.go +++ b/pkg/bindings/pods/pods.go @@ -2,6 +2,7 @@ package pods import ( "context" + "errors" "net/http" "net/url" "strconv" @@ -189,11 +190,6 @@ func Start(ctx context.Context, nameOrID string) (*entities.PodStartReport, erro return &report, response.Process(&report) } -func Stats() error { - // TODO - return bindings.ErrNotImplemented -} - // Stop stops all containers in a Pod. The optional timeout parameter can be // used to override the timeout before the container is killed. func Stop(ctx context.Context, nameOrID string, timeout *int) (*entities.PodStopReport, error) { @@ -264,3 +260,26 @@ func Unpause(ctx context.Context, nameOrID string) (*entities.PodUnpauseReport, } return &report, response.Process(&report) } + +// Stats display resource-usage statistics of one or more pods. +func Stats(ctx context.Context, namesOrIDs []string, options entities.PodStatsOptions) ([]*entities.PodStatsReport, error) { + if options.Latest { + return nil, errors.New("latest is not supported") + } + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + params := url.Values{} + for _, i := range namesOrIDs { + params.Add("namesOrIDs", i) + } + params.Set("all", strconv.FormatBool(options.All)) + + var reports []*entities.PodStatsReport + response, err := conn.DoRequest(nil, http.MethodGet, "/pods/stats", params) + if err != nil { + return nil, err + } + return reports, response.Process(&reports) +} diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index 8c5bc3058..502279bcf 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -53,6 +53,7 @@ type ContainerEngine interface { PodRestart(ctx context.Context, namesOrIds []string, options PodRestartOptions) ([]*PodRestartReport, error) PodRm(ctx context.Context, namesOrIds []string, options PodRmOptions) ([]*PodRmReport, error) PodStart(ctx context.Context, namesOrIds []string, options PodStartOptions) ([]*PodStartReport, error) + PodStats(ctx context.Context, namesOrIds []string, options PodStatsOptions) ([]*PodStatsReport, error) PodStop(ctx context.Context, namesOrIds []string, options PodStopOptions) ([]*PodStopReport, error) PodTop(ctx context.Context, options PodTopOptions) (*StringSliceReport, error) PodUnpause(ctx context.Context, namesOrIds []string, options PodunpauseOptions) ([]*PodUnpauseReport, error) diff --git a/pkg/domain/entities/manifest.go b/pkg/domain/entities/manifest.go index a9c961f9d..7316735b0 100644 --- a/pkg/domain/entities/manifest.go +++ b/pkg/domain/entities/manifest.go @@ -10,6 +10,7 @@ type ManifestAddOptions struct { Arch string `json:"arch" schema:"arch"` Features []string `json:"features" schema:"features"` Images []string `json:"images" schema:"images"` + OS string `json:"os" schema:"os"` OSVersion string `json:"os_version" schema:"os_version"` Variant string `json:"variant" schema:"variant"` } diff --git a/pkg/domain/entities/pods.go b/pkg/domain/entities/pods.go index aa1445a6a..a4896ce4d 100644 --- a/pkg/domain/entities/pods.go +++ b/pkg/domain/entities/pods.go @@ -1,6 +1,7 @@ package entities import ( + "errors" "strings" "time" @@ -188,3 +189,50 @@ type PodInspectOptions struct { type PodInspectReport struct { *define.InspectPodData } + +// PodStatsOptions are options for the pod stats command. +type PodStatsOptions struct { + // All - provide stats for all running pods. + All bool + // Latest - provide stats for the latest pod. + Latest bool +} + +// PodStatsReport includes pod-resource statistics data. +type PodStatsReport struct { + CPU string + MemUsage string + Mem string + NetIO string + BlockIO string + PIDS string + Pod string + CID string + Name string +} + +// ValidatePodStatsOptions validates the specified slice and options. Allows +// for sharing code in the front- and the back-end. +func ValidatePodStatsOptions(args []string, options *PodStatsOptions) error { + num := 0 + if len(args) > 0 { + num++ + } + if options.All { + num++ + } + if options.Latest { + num++ + } + switch num { + case 0: + // Podman v1 compat: if nothing's specified get all running + // pods. + options.All = true + return nil + case 1: + return nil + default: + return errors.New("--all, --latest and arguments cannot be used together") + } +} diff --git a/pkg/domain/infra/abi/manifest.go b/pkg/domain/infra/abi/manifest.go index 27d4bf9a5..88331f96c 100644 --- a/pkg/domain/infra/abi/manifest.go +++ b/pkg/domain/infra/abi/manifest.go @@ -79,6 +79,7 @@ func (ir *ImageEngine) ManifestAdd(ctx context.Context, opts entities.ManifestAd Arch: opts.Arch, Features: opts.Features, Images: opts.Images, + OS: opts.OS, OSVersion: opts.OSVersion, Variant: opts.Variant, } diff --git a/pkg/domain/infra/abi/pods.go b/pkg/domain/infra/abi/pods.go index c4ae9efbf..7c06f9a4e 100644 --- a/pkg/domain/infra/abi/pods.go +++ b/pkg/domain/infra/abi/pods.go @@ -145,7 +145,7 @@ func (ic *ContainerEngine) PodStop(ctx context.Context, namesOrIds []string, opt reports []*entities.PodStopReport ) pods, err := getPodsByContext(options.All, options.Latest, namesOrIds, ic.Libpod) - if err != nil { + if err != nil && !(options.Ignore && errors.Cause(err) == define.ErrNoSuchPod) { return nil, err } for _, p := range pods { @@ -180,6 +180,7 @@ func (ic *ContainerEngine) PodRestart(ctx context.Context, namesOrIds []string, errs, err := p.Restart(ctx) if err != nil { report.Errs = []error{err} + reports = append(reports, &report) continue } if len(errs) > 0 { @@ -207,6 +208,7 @@ func (ic *ContainerEngine) PodStart(ctx context.Context, namesOrIds []string, op errs, err := p.Start(ctx) if err != nil { report.Errs = []error{err} + reports = append(reports, &report) continue } if len(errs) > 0 { @@ -226,7 +228,7 @@ func (ic *ContainerEngine) PodRm(ctx context.Context, namesOrIds []string, optio reports []*entities.PodRmReport ) pods, err := getPodsByContext(options.All, options.Latest, namesOrIds, ic.Libpod) - if err != nil { + if err != nil && !(options.Ignore && errors.Cause(err) == define.ErrNoSuchPod) { return nil, err } for _, p := range pods { @@ -234,6 +236,7 @@ func (ic *ContainerEngine) PodRm(ctx context.Context, namesOrIds []string, optio err := ic.Libpod.RemovePod(ctx, p, true, options.Force) if err != nil { report.Err = err + reports = append(reports, &report) continue } reports = append(reports, &report) @@ -292,9 +295,12 @@ func (ic *ContainerEngine) PodTop(ctx context.Context, options entities.PodTopOp func (ic *ContainerEngine) PodPs(ctx context.Context, options entities.PodPSOptions) ([]*entities.ListPodsReport, error) { var ( + err error filters []libpod.PodFilter + pds []*libpod.Pod reports []*entities.ListPodsReport ) + for k, v := range options.Filters { for _, filter := range v { f, err := lpfilters.GeneratePodFilterFunc(k, filter) @@ -305,10 +311,19 @@ func (ic *ContainerEngine) PodPs(ctx context.Context, options entities.PodPSOpti } } - pds, err := ic.Libpod.Pods(filters...) - if err != nil { - return nil, err + if options.Latest { + pod, err := ic.Libpod.GetLatestPod() + if err != nil { + return nil, err + } + pds = append(pds, pod) + } else { + pds, err = ic.Libpod.Pods(filters...) + if err != nil { + return nil, err + } } + for _, p := range pds { var lpcs []*entities.ListPodContainer status, err := p.GetPodStatus() diff --git a/pkg/domain/infra/abi/pods_stats.go b/pkg/domain/infra/abi/pods_stats.go new file mode 100644 index 000000000..a41c01da0 --- /dev/null +++ b/pkg/domain/infra/abi/pods_stats.go @@ -0,0 +1,85 @@ +package abi + +import ( + "context" + "fmt" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/cgroups" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/rootless" + "github.com/docker/go-units" + "github.com/pkg/errors" +) + +// PodStats implements printing stats about pods. +func (ic *ContainerEngine) PodStats(ctx context.Context, namesOrIds []string, options entities.PodStatsOptions) ([]*entities.PodStatsReport, error) { + // Cgroups v2 check for rootless. + if rootless.IsRootless() { + unified, err := cgroups.IsCgroup2UnifiedMode() + if err != nil { + return nil, err + } + if !unified { + return nil, errors.New("pod stats is not supported in rootless mode without cgroups v2") + } + } + // Get the (running) pods and convert them to the entities format. + pods, err := getPodsByContext(options.All, options.Latest, namesOrIds, ic.Libpod) + if err != nil { + return nil, errors.Wrap(err, "unable to get list of pods") + } + return ic.podsToStatsReport(pods) +} + +// podsToStatsReport converts a slice of pods into a corresponding slice of stats reports. +func (ic *ContainerEngine) podsToStatsReport(pods []*libpod.Pod) ([]*entities.PodStatsReport, error) { + reports := []*entities.PodStatsReport{} + for i := range pods { // Access by index to prevent potential loop-variable leaks. + podStats, err := pods[i].GetPodStats(nil) + if err != nil { + return nil, err + } + podID := pods[i].ID()[:12] + for j := range podStats { + r := entities.PodStatsReport{ + CPU: floatToPercentString(podStats[j].CPU), + MemUsage: combineHumanValues(podStats[j].MemUsage, podStats[j].MemLimit), + Mem: floatToPercentString(podStats[j].MemPerc), + NetIO: combineHumanValues(podStats[j].NetInput, podStats[j].NetOutput), + BlockIO: combineHumanValues(podStats[j].BlockInput, podStats[j].BlockOutput), + PIDS: pidsToString(podStats[j].PIDs), + CID: podStats[j].ContainerID[:12], + Name: podStats[j].Name, + Pod: podID, + } + reports = append(reports, &r) + } + } + + return reports, nil +} + +func combineHumanValues(a, b uint64) string { + if a == 0 && b == 0 { + return "-- / --" + } + return fmt.Sprintf("%s / %s", units.HumanSize(float64(a)), units.HumanSize(float64(b))) +} + +func floatToPercentString(f float64) string { + strippedFloat, err := libpod.RemoveScientificNotationFromFloat(f) + if err != nil || strippedFloat == 0 { + // If things go bazinga, return a safe value + return "--" + } + return fmt.Sprintf("%.2f", strippedFloat) + "%" +} + +func pidsToString(pid uint64) string { + if pid == 0 { + // If things go bazinga, return a safe value + return "--" + } + return fmt.Sprintf("%d", pid) +} diff --git a/pkg/domain/infra/tunnel/manifest.go b/pkg/domain/infra/tunnel/manifest.go index 338256530..18b400533 100644 --- a/pkg/domain/infra/tunnel/manifest.go +++ b/pkg/domain/infra/tunnel/manifest.go @@ -41,6 +41,7 @@ func (ir *ImageEngine) ManifestAdd(ctx context.Context, opts entities.ManifestAd Arch: opts.Arch, Features: opts.Features, Images: opts.Images, + OS: opts.OS, OSVersion: opts.OSVersion, Variant: opts.Variant, } diff --git a/pkg/domain/infra/tunnel/pods.go b/pkg/domain/infra/tunnel/pods.go index e7641c077..c193c6752 100644 --- a/pkg/domain/infra/tunnel/pods.go +++ b/pkg/domain/infra/tunnel/pods.go @@ -211,3 +211,7 @@ func (ic *ContainerEngine) PodInspect(ctx context.Context, options entities.PodI } return pods.Inspect(ic.ClientCxt, options.NameOrID) } + +func (ic *ContainerEngine) PodStats(ctx context.Context, namesOrIds []string, options entities.PodStatsOptions) ([]*entities.PodStatsReport, error) { + return pods.Stats(ic.ClientCxt, namesOrIds, options) +} diff --git a/pkg/specgen/generate/container.go b/pkg/specgen/generate/container.go index 9797ad572..669b1f05f 100644 --- a/pkg/specgen/generate/container.go +++ b/pkg/specgen/generate/container.go @@ -25,6 +25,13 @@ func CompleteSpec(ctx context.Context, r *libpod.Runtime, s *specgen.SpecGenerat return err } + if s.HealthConfig == nil { + s.HealthConfig, err = newImage.GetHealthCheck(ctx) + if err != nil { + return err + } + } + // Image stop signal if s.StopSignal == nil { stopSignal, err := newImage.StopSignal(ctx) diff --git a/pkg/specgen/generate/pod_create.go b/pkg/specgen/generate/pod_create.go index 292f9b155..babfba9bc 100644 --- a/pkg/specgen/generate/pod_create.go +++ b/pkg/specgen/generate/pod_create.go @@ -46,6 +46,13 @@ func createPodOptions(p *specgen.PodSpecGenerator) ([]libpod.PodCreateOption, er if len(p.HostAdd) > 0 { options = append(options, libpod.WithPodHosts(p.HostAdd)) } + if len(p.DNSServer) > 0 { + var dnsServers []string + for _, d := range p.DNSServer { + dnsServers = append(dnsServers, d.String()) + } + options = append(options, libpod.WithPodDNS(dnsServers)) + } if len(p.DNSOption) > 0 { options = append(options, libpod.WithPodDNSOption(p.DNSOption)) } diff --git a/pkg/specgen/pod_validate.go b/pkg/specgen/pod_validate.go index f2f90e58d..98d59549e 100644 --- a/pkg/specgen/pod_validate.go +++ b/pkg/specgen/pod_validate.go @@ -62,7 +62,7 @@ func (p *PodSpecGenerator) Validate() error { return exclusivePodOptions("NoInfra", "NoManageResolvConf") } } - if p.NetNS.NSMode != Bridge { + if p.NetNS.NSMode != "" && p.NetNS.NSMode != Bridge && p.NetNS.NSMode != Default { if len(p.PortMappings) > 0 { return errors.New("PortMappings can only be used with Bridge mode networking") } diff --git a/test/e2e/common_test.go b/test/e2e/common_test.go index d93ee8d3a..160af1bd5 100644 --- a/test/e2e/common_test.go +++ b/test/e2e/common_test.go @@ -14,7 +14,6 @@ import ( "testing" "time" - "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/inspect" "github.com/containers/libpod/pkg/rootless" @@ -501,8 +500,8 @@ func (s *PodmanSessionIntegration) InspectContainerToJSON() []define.InspectCont } // InspectPodToJSON takes the sessions output from a pod inspect and returns json -func (s *PodmanSessionIntegration) InspectPodToJSON() libpod.PodInspect { - var i libpod.PodInspect +func (s *PodmanSessionIntegration) InspectPodToJSON() define.InspectPodData { + var i define.InspectPodData err := json.Unmarshal(s.Out.Contents(), &i) Expect(err).To(BeNil()) return i diff --git a/test/e2e/container_inspect_test.go b/test/e2e/container_inspect_test.go index cc986f1a8..91c025197 100644 --- a/test/e2e/container_inspect_test.go +++ b/test/e2e/container_inspect_test.go @@ -17,7 +17,6 @@ var _ = Describe("Podman container inspect", func() { ) BeforeEach(func() { - Skip(v2fail) tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) diff --git a/test/e2e/healthcheck_run_test.go b/test/e2e/healthcheck_run_test.go index 58d473ca8..19a8658ac 100644 --- a/test/e2e/healthcheck_run_test.go +++ b/test/e2e/healthcheck_run_test.go @@ -18,7 +18,6 @@ var _ = Describe("Podman healthcheck run", func() { ) BeforeEach(func() { - Skip(v2fail) tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) diff --git a/test/e2e/manifest_test.go b/test/e2e/manifest_test.go index a52916e87..9b5a24771 100644 --- a/test/e2e/manifest_test.go +++ b/test/e2e/manifest_test.go @@ -85,4 +85,17 @@ var _ = Describe("Podman manifest", func() { Expect(session.OutputToString()).To(ContainSubstring(imageListPPC64LEInstanceDigest)) Expect(session.OutputToString()).To(ContainSubstring(imageListS390XInstanceDigest)) }) + + It("podman manifest add --os", func() { + session := podmanTest.Podman([]string{"manifest", "create", "foo"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + session = podmanTest.Podman([]string{"manifest", "add", "--os", "bar", "foo", imageList}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + session = podmanTest.Podman([]string{"manifest", "inspect", "foo"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(ContainSubstring(`"os": "bar"`)) + }) }) diff --git a/test/e2e/pause_test.go b/test/e2e/pause_test.go index 66b888803..149a2e28a 100644 --- a/test/e2e/pause_test.go +++ b/test/e2e/pause_test.go @@ -2,7 +2,10 @@ package integration import ( "fmt" + "io/ioutil" "os" + "path/filepath" + "strings" "github.com/containers/libpod/pkg/cgroups" . "github.com/containers/libpod/test/utils" @@ -17,11 +20,10 @@ var _ = Describe("Podman pause", func() { podmanTest *PodmanTestIntegration ) - pausedState := "Paused" - createdState := "Created" + pausedState := "paused" + createdState := "created" BeforeEach(func() { - Skip(v2fail) SkipIfRootless() tempdir, err = CreateTempDirInTempDir() if err != nil { @@ -32,7 +34,13 @@ var _ = Describe("Podman pause", func() { Expect(err).To(BeNil()) if cgroupsv2 { - _, err := os.Stat("/sys/fs/cgroup/cgroup.freeze") + b, err := ioutil.ReadFile("/proc/self/cgroup") + if err != nil { + Skip("cannot read self cgroup") + } + + path := filepath.Join("/sys/fs/cgroup", strings.TrimSuffix(strings.Replace(string(b), "0::", "", 1), "\n"), "cgroup.freeze") + _, err = os.Stat(path) if err != nil { Skip("freezer controller not available on the current kernel") } @@ -73,7 +81,7 @@ var _ = Describe("Podman pause", func() { Expect(result).To(ExitWithError()) Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) - Expect(podmanTest.GetContainerStatus()).To(ContainSubstring(createdState)) + Expect(strings.ToLower(podmanTest.GetContainerStatus())).To(ContainSubstring(createdState)) }) It("podman pause a running container by id", func() { @@ -86,7 +94,7 @@ var _ = Describe("Podman pause", func() { Expect(result.ExitCode()).To(Equal(0)) Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) - Expect(podmanTest.GetContainerStatus()).To(ContainSubstring(pausedState)) + Expect(strings.ToLower(podmanTest.GetContainerStatus())).To(ContainSubstring(pausedState)) result = podmanTest.Podman([]string{"unpause", cid}) result.WaitWithDefaultTimeout() @@ -103,7 +111,7 @@ var _ = Describe("Podman pause", func() { Expect(result.ExitCode()).To(Equal(0)) Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) - Expect(podmanTest.GetContainerStatus()).To(ContainSubstring(pausedState)) + Expect(strings.ToLower(podmanTest.GetContainerStatus())).To(ContainSubstring(pausedState)) result = podmanTest.Podman([]string{"container", "unpause", cid}) result.WaitWithDefaultTimeout() @@ -134,14 +142,14 @@ var _ = Describe("Podman pause", func() { Expect(result.ExitCode()).To(Equal(0)) Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) - Expect(podmanTest.GetContainerStatus()).To(ContainSubstring(pausedState)) + Expect(strings.ToLower(podmanTest.GetContainerStatus())).To(ContainSubstring(pausedState)) result = podmanTest.Podman([]string{"rm", cid}) result.WaitWithDefaultTimeout() Expect(result.ExitCode()).To(Equal(2)) Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) - Expect(podmanTest.GetContainerStatus()).To(ContainSubstring(pausedState)) + Expect(strings.ToLower(podmanTest.GetContainerStatus())).To(ContainSubstring(pausedState)) }) @@ -156,7 +164,7 @@ var _ = Describe("Podman pause", func() { Expect(result.ExitCode()).To(Equal(0)) Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) - Expect(podmanTest.GetContainerStatus()).To(ContainSubstring(pausedState)) + Expect(strings.ToLower(podmanTest.GetContainerStatus())).To(ContainSubstring(pausedState)) result = podmanTest.Podman([]string{"rm", "--force", cid}) result.WaitWithDefaultTimeout() @@ -176,14 +184,14 @@ var _ = Describe("Podman pause", func() { Expect(result.ExitCode()).To(Equal(0)) Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) - Expect(podmanTest.GetContainerStatus()).To(ContainSubstring(pausedState)) + Expect(strings.ToLower(podmanTest.GetContainerStatus())).To(ContainSubstring(pausedState)) result = podmanTest.Podman([]string{"stop", cid}) result.WaitWithDefaultTimeout() Expect(result.ExitCode()).To(Equal(125)) Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) - Expect(podmanTest.GetContainerStatus()).To(ContainSubstring(pausedState)) + Expect(strings.ToLower(podmanTest.GetContainerStatus())).To(ContainSubstring(pausedState)) result = podmanTest.Podman([]string{"unpause", cid}) result.WaitWithDefaultTimeout() @@ -212,7 +220,7 @@ var _ = Describe("Podman pause", func() { Expect(result.ExitCode()).To(Equal(0)) Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) - Expect(podmanTest.GetContainerStatus()).To(Equal(pausedState)) + Expect(strings.ToLower(podmanTest.GetContainerStatus())).To(Equal(pausedState)) result = podmanTest.Podman([]string{"unpause", "test1"}) result.WaitWithDefaultTimeout() diff --git a/test/e2e/pod_create_test.go b/test/e2e/pod_create_test.go index 30abe2be2..e0a10c202 100644 --- a/test/e2e/pod_create_test.go +++ b/test/e2e/pod_create_test.go @@ -18,7 +18,6 @@ var _ = Describe("Podman pod create", func() { ) BeforeEach(func() { - Skip(v2fail) tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) diff --git a/test/e2e/pod_inspect_test.go b/test/e2e/pod_inspect_test.go index 06f36c751..f87bbe047 100644 --- a/test/e2e/pod_inspect_test.go +++ b/test/e2e/pod_inspect_test.go @@ -16,7 +16,6 @@ var _ = Describe("Podman pod inspect", func() { ) BeforeEach(func() { - Skip(v2fail) tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) @@ -55,8 +54,7 @@ var _ = Describe("Podman pod inspect", func() { inspect.WaitWithDefaultTimeout() Expect(inspect.ExitCode()).To(Equal(0)) Expect(inspect.IsJSONOutputValid()).To(BeTrue()) - // FIXME sujil, disabled for now - //podData := inspect.InspectPodToJSON() - //Expect(podData.Config.ID).To(Equal(podid)) + podData := inspect.InspectPodToJSON() + Expect(podData.ID).To(Equal(podid)) }) }) diff --git a/test/e2e/pod_kill_test.go b/test/e2e/pod_kill_test.go index 29d7664df..a3efec46c 100644 --- a/test/e2e/pod_kill_test.go +++ b/test/e2e/pod_kill_test.go @@ -17,7 +17,6 @@ var _ = Describe("Podman pod kill", func() { ) BeforeEach(func() { - Skip(v2fail) tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) diff --git a/test/e2e/pod_pause_test.go b/test/e2e/pod_pause_test.go index bb1719203..7067c9a87 100644 --- a/test/e2e/pod_pause_test.go +++ b/test/e2e/pod_pause_test.go @@ -15,10 +15,9 @@ var _ = Describe("Podman pod pause", func() { podmanTest *PodmanTestIntegration ) - pausedState := "Paused" + pausedState := "paused" BeforeEach(func() { - Skip(v2fail) SkipIfRootless() tempdir, err = CreateTempDirInTempDir() if err != nil { diff --git a/test/e2e/pod_prune_test.go b/test/e2e/pod_prune_test.go index d0725883c..d98383331 100644 --- a/test/e2e/pod_prune_test.go +++ b/test/e2e/pod_prune_test.go @@ -16,7 +16,6 @@ var _ = Describe("Podman pod prune", func() { ) BeforeEach(func() { - Skip(v2fail) tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) diff --git a/test/e2e/pod_ps_test.go b/test/e2e/pod_ps_test.go index ea9118f37..5f8712a7a 100644 --- a/test/e2e/pod_ps_test.go +++ b/test/e2e/pod_ps_test.go @@ -18,7 +18,6 @@ var _ = Describe("Podman ps", func() { ) BeforeEach(func() { - Skip(v2fail) tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) @@ -96,6 +95,7 @@ var _ = Describe("Podman ps", func() { Expect(result.OutputToString()).To(ContainSubstring(podid2)) Expect(result.OutputToString()).To(Not(ContainSubstring(podid1))) }) + It("podman pod ps id filter flag", func() { _, ec, podid := podmanTest.CreatePod("") Expect(ec).To(Equal(0)) @@ -143,7 +143,7 @@ var _ = Describe("Podman ps", func() { _, ec, _ = podmanTest.RunLsContainerInPod("test2", podid) Expect(ec).To(Equal(0)) - session = podmanTest.Podman([]string{"pod", "ps", "--format={{.ContainerInfo}}", "--ctr-names"}) + session = podmanTest.Podman([]string{"pod", "ps", "--format={{.ContainerNames}}", "--ctr-names"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) Expect(session.OutputToString()).To(ContainSubstring("test1")) @@ -228,4 +228,18 @@ var _ = Describe("Podman ps", func() { Expect(session.OutputToString()).To(ContainSubstring(podid2)) Expect(session.OutputToString()).To(Not(ContainSubstring(podid3))) }) + + It("pod no infra should ps", func() { + session := podmanTest.Podman([]string{"pod", "create", "--infra=false"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + ps := podmanTest.Podman([]string{"pod", "ps"}) + ps.WaitWithDefaultTimeout() + Expect(ps.ExitCode()).To(Equal(0)) + + infra := podmanTest.Podman([]string{"pod", "ps", "--format", "{{.InfraId}}"}) + infra.WaitWithDefaultTimeout() + Expect(len(infra.OutputToString())).To(BeZero()) + }) }) diff --git a/test/e2e/pod_restart_test.go b/test/e2e/pod_restart_test.go index 9938c70b8..691fe5f0c 100644 --- a/test/e2e/pod_restart_test.go +++ b/test/e2e/pod_restart_test.go @@ -16,7 +16,6 @@ var _ = Describe("Podman pod restart", func() { ) BeforeEach(func() { - Skip(v2fail) tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) diff --git a/test/e2e/pod_rm_test.go b/test/e2e/pod_rm_test.go index 117b54987..90f178be6 100644 --- a/test/e2e/pod_rm_test.go +++ b/test/e2e/pod_rm_test.go @@ -19,7 +19,6 @@ var _ = Describe("Podman pod rm", func() { ) BeforeEach(func() { - Skip(v2fail) tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) diff --git a/test/e2e/pod_stats_test.go b/test/e2e/pod_stats_test.go index bb3610a27..347f33e62 100644 --- a/test/e2e/pod_stats_test.go +++ b/test/e2e/pod_stats_test.go @@ -18,7 +18,6 @@ var _ = Describe("Podman pod stats", func() { ) BeforeEach(func() { - Skip(v2fail) cgroupsv2, err := cgroups.IsCgroup2UnifiedMode() Expect(err).To(BeNil()) diff --git a/test/e2e/pod_stop_test.go b/test/e2e/pod_stop_test.go index 0c0085b82..a61917adb 100644 --- a/test/e2e/pod_stop_test.go +++ b/test/e2e/pod_stop_test.go @@ -16,7 +16,6 @@ var _ = Describe("Podman pod stop", func() { ) BeforeEach(func() { - Skip(v2fail) tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) diff --git a/test/e2e/pod_top_test.go b/test/e2e/pod_top_test.go index 2f75aaf30..c313b0675 100644 --- a/test/e2e/pod_top_test.go +++ b/test/e2e/pod_top_test.go @@ -20,7 +20,6 @@ var _ = Describe("Podman top", func() { ) BeforeEach(func() { - Skip(v2fail) tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) diff --git a/test/e2e/run_dns_test.go b/test/e2e/run_dns_test.go index 749047b76..02b9ff8d1 100644 --- a/test/e2e/run_dns_test.go +++ b/test/e2e/run_dns_test.go @@ -18,7 +18,6 @@ var _ = Describe("Podman run dns", func() { ) BeforeEach(func() { - Skip(v2fail) tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) diff --git a/test/e2e/run_ns_test.go b/test/e2e/run_ns_test.go index 9c914188a..c8ba68efc 100644 --- a/test/e2e/run_ns_test.go +++ b/test/e2e/run_ns_test.go @@ -19,7 +19,6 @@ var _ = Describe("Podman run ns", func() { ) BeforeEach(func() { - Skip(v2fail) tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) diff --git a/test/e2e/run_passwd_test.go b/test/e2e/run_passwd_test.go index 0868bce4f..bd6a0e036 100644 --- a/test/e2e/run_passwd_test.go +++ b/test/e2e/run_passwd_test.go @@ -18,7 +18,6 @@ var _ = Describe("Podman run passwd", func() { ) BeforeEach(func() { - Skip(v2fail) tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) diff --git a/test/e2e/run_restart_test.go b/test/e2e/run_restart_test.go index 28ab23ab0..8bbdf2056 100644 --- a/test/e2e/run_restart_test.go +++ b/test/e2e/run_restart_test.go @@ -18,7 +18,6 @@ var _ = Describe("Podman run restart containers", func() { ) BeforeEach(func() { - Skip(v2fail) tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) diff --git a/test/e2e/run_signal_test.go b/test/e2e/run_signal_test.go index 58dde62da..fbdd3acec 100644 --- a/test/e2e/run_signal_test.go +++ b/test/e2e/run_signal_test.go @@ -29,7 +29,6 @@ var _ = Describe("Podman run with --sig-proxy", func() { ) BeforeEach(func() { - Skip(v2fail) tmpdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) diff --git a/vendor/github.com/rootless-containers/rootlesskit/pkg/port/builtin/parent/parent.go b/vendor/github.com/rootless-containers/rootlesskit/pkg/port/builtin/parent/parent.go index 893bf1da9..8ffadd859 100644 --- a/vendor/github.com/rootless-containers/rootlesskit/pkg/port/builtin/parent/parent.go +++ b/vendor/github.com/rootless-containers/rootlesskit/pkg/port/builtin/parent/parent.go @@ -2,11 +2,14 @@ package parent import ( "context" + "fmt" "io" "io/ioutil" "net" "os" "path/filepath" + "strconv" + "strings" "sync" "syscall" @@ -84,6 +87,39 @@ func (d *driver) RunParentDriver(initComplete chan struct{}, quit <-chan struct{ return nil } +func isEPERM(err error) bool { + k := "permission denied" + // As of Go 1.14, errors.Is(err, syscall.EPERM) does not seem to work for + // "listen tcp 0.0.0.0:80: bind: permission denied" error from net.ListenTCP(). + return errors.Is(err, syscall.EPERM) || strings.Contains(err.Error(), k) +} + +// annotateEPERM annotates origErr for human-readability +func annotateEPERM(origErr error, spec port.Spec) error { + // Read "net.ipv4.ip_unprivileged_port_start" value (typically 1024) + // TODO: what for IPv6? + // NOTE: sync.Once should not be used here + b, e := ioutil.ReadFile("/proc/sys/net/ipv4/ip_unprivileged_port_start") + if e != nil { + return origErr + } + start, e := strconv.Atoi(strings.TrimSpace(string(b))) + if e != nil { + return origErr + } + if spec.ParentPort >= start { + // origErr is unrelated to ip_unprivileged_port_start + return origErr + } + text := fmt.Sprintf("cannot expose privileged port %d, you might need to add \"net.ipv4.ip_unprivileged_port_start=0\" (currently %d) to /etc/sysctl.conf", spec.ParentPort, start) + if filepath.Base(os.Args[0]) == "rootlesskit" { + // NOTE: The following sentence is appended only if Args[0] == "rootlesskit", because it does not apply to Podman (as of Podman v1.9). + // Podman launches the parent driver in the child user namespace (but in the parent network namespace), which disables the file capability. + text += ", or set CAP_NET_BIND_SERVICE on rootlesskit binary" + } + return errors.Wrap(origErr, text) +} + func (d *driver) AddPort(ctx context.Context, spec port.Spec) (*port.Status, error) { d.mu.Lock() err := portutil.ValidatePortSpec(spec, d.ports) @@ -106,6 +142,9 @@ func (d *driver) AddPort(ctx context.Context, spec port.Spec) (*port.Status, err return nil, errors.New("spec was not validated?") } if err != nil { + if isEPERM(err) { + err = annotateEPERM(err, spec) + } return nil, err } d.mu.Lock() diff --git a/vendor/modules.txt b/vendor/modules.txt index 782a905a1..0a6d8ccd5 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -454,7 +454,7 @@ github.com/prometheus/common/model github.com/prometheus/procfs github.com/prometheus/procfs/internal/fs github.com/prometheus/procfs/internal/util -# github.com/rootless-containers/rootlesskit v0.9.3 +# github.com/rootless-containers/rootlesskit v0.9.4 github.com/rootless-containers/rootlesskit/pkg/msgutil github.com/rootless-containers/rootlesskit/pkg/port github.com/rootless-containers/rootlesskit/pkg/port/builtin |