diff options
93 files changed, 3132 insertions, 1210 deletions
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 246a4db04..aef66545f 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -18,9 +18,11 @@ - Fixed a bug where `podman play kube` would not properly handle container-only port mappings ([#5610](https://github.com/containers/libpod/issues/5610)) - Fixed a bug where the `podman container prune` command was not pruning containers in the `created` and `configured` states - Fixed a bug where Podman was not properly removing CNI IP address allocations after a reboot ([#5433](https://github.com/containers/libpod/issues/5433)) +- Fixed a bug where Podman was not properly applying the default Seccomp profile when `--security-opt` was not given at the command line ### HTTP API -- Many Libpod API endpoints have been added, including `Changes`, `Checkpoint`, and `Restore` +- Many Libpod API endpoints have been added, including `Changes`, `Checkpoint`, `Init`, and `Restore` +- Resolved issues where the `podman system service` command would time out and exit while there were still active connections - Stability overall has greatly improved as we prepare the API for a beta release soon with Podman 2.0 ### Misc diff --git a/changelog.txt b/changelog.txt index 1e08a8419..95f8d5b47 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,190 @@ +- Changelog for v1.9.0 (2020-04-15) + * podmanV2: fix nil deref + * v2specgen prune libpod + * More system test fixes on regressions + * Add support for the global flags and config files + * Bump to v1.9.0-dev + +- Changelog for v1.9.0-rc2 (2020-04-14) + * Update release notes for v1.9.0-RC2 + * v2podman ps revert structure changes + * podmanv2 mount and umount + * Fix invalid container path comparison for pid cgroup + * v2podman add container init + * Need to set security options even if user does not specify options + * podmanv2 version format variable name change + * Fixes for load and other system tests + * Improve APIv2 support for Attach + * Refactor service idle support + * podmanv2 history and image remove templates + * Bump to v1.9.0-dev + * rootless: use snprintf + * podmanV2: implement search + +- Changelog for v1.9.0-rc1 (2020-04-13) + * build(deps): bump github.com/containers/buildah from 1.14.7 to 1.14.8 + * Update release notes for v1.9.0-RC1 + * v2podman container cleanup + * podmanV2: implement logs + * test: enable preserve fds test for crun + * test: fix exec preserve-fds test + * Set exit codes on errors. + * Run (make vendor) + * Fix (make vendor) + * update the latest version to 1.8.2 + * add tests for kill and exists + * v2podman ps alter formats + * run/create were processing options after the image name + * V2 podman system service + * man page: add note about issue with SELinux + * Bump Buildah to v1.14.7 + * Bump containers/image to v5.4.3 + * V2 podman diff(changes) support + * podman info needs to be run within the user namespace + * podmanv2 images user format + * podmanv2 info + * vendor c/image v5.4.2 + * Do not error on pids.current stats if ctr.path is empty + * fix rootless login/logout tests + * v2podman run + * refactor info + * podmanv2 ps + * userns: support --userns=auto + * podmanv2 start + * build(deps): bump github.com/containers/common from 0.8.0 to 0.8.1 + * build(deps): bump github.com/containers/storage from 1.18.1 to 1.18.2 + * build(deps): bump github.com/opencontainers/selinux from 1.4.0 to 1.5.0 + * v2podman attach and exec + * v2podman container create + * Cleanup whether to enter user namespace for rootless commands + * podmanv2 save image + * podmanv2 version + * checkpoint: handle XDG_RUNTIME_DIR + * checkpoint: change runtime checkpoint support test + * Pass path environment down to the OCI runtime + * podmanv2 checkpoint and restore + * Bump github.com/containers/common from 0.6.1 to 0.8.0 + * test/e2e/run_volume_test: use unique mount point + * test/e2e/run_volume_test.go: mv dockerfile decl + * test/e2e/run_volume_test: only create dir once + * Fix environment handling from containers.conf + * podmanV2: implement push + * pkg/spec.InitFSMounts: optimize + * utils: delete dead code + * attach: skip shutdown on errors + * attach: fix hang if control path is deleted + * pkg/spec.InitFSMounts: fix mount opts in place + * podmanv2 export + * podmanv2 import + * podmanv2-retry - new helper for testing v2 + * podmanv2 load + * podmanv2 pod inspect + * V2 podman inspect + * Fix repos for CentOS 7 RPM build + * podman v2 image tag and untag + * podmanv2 pod ps + * Touch up mailing list address in README.md + * add systemd build tag to podman builds + * Bump github.com/rootless-containers/rootlesskit from 0.9.2 to 0.9.3 + * Switch to using --time as opposed to --timeout to better match Docker. + * podmanV2: implement pull + * pkg/spec/initFSMounts: fix + * Cirrus: Remove darwin/windows builds in gate-job + * Cirrus: Update VM Images + * Cirrus: Minor docs update + * Revert "Default CPUShares in Inspect are 1024" + * fix more swagger inconsistencies + * V2 Move varlink home + * Bump github.com/containers/conmon + * Bump github.com/spf13/cobra from 0.0.6 to 0.0.7 + * rootless: make cgroup ownership detection not fatal + * podmanv2 enable healthcheck run + * Update vendor of boltdb and containers/image + * swagger: top: remove "Docker" from the identifiers + * podmanv2: implement pod top + * v2 api: implement pods top endpoint + * podmanv2 commit + * Bump to buildah v1.14.5 + * Add support for containers.conf + * API v2 tests: usability improvements + * Sanitize port parsing for pods in play kube + * podmanv2 pod create using podspecgen + * use `pause:3.2` image for infra containers + * Add support for specifying CNI networks in podman play kube + * Fix typo in pod create + * podmanV2: implement top + * Fix Markdown typo in podman-create.1.md + * V2 podman image prune + * Support label filters for podman pod ps. + * podmanv2 container inspect + * podmanv2 pod subcommands + * Add bindings for Container Exec Create + Inspect + * apiv2 add default network in specgen + * slirp: enable seccomp filter + * V2 podman image rm | podman rmi [IMAGE] + * V2 podman image + * podmanv2 add pre-run to each commmand + * Ensure that exec sends resize events + * enable linting on v2 + * Bump github.com/rootless-containers/rootlesskit from 0.8.0 to 0.9.2 + * Bump github.com/containers/storage from 1.16.5 to 1.16.6 + * V2 podman images/image list + * podmanv2 volumes + * Combine GlobalFlags and EngineFlags into EngineOptions + * Complete podmanV2 history command + * rootlessport: use x/sys/unix instead of syscall + * podmanv2 exit code + * Bump github.com/sirupsen/logrus from 1.4.2 to 1.5.0 + * Correctly document libpod commit endpoint + * Implement APIv2 Exec Create and Inspect Endpoints + * apiv2 container commit for libpod + * Add image signing with GPG tutorial + * podmanv2 add core container commands + * Improved readability in image json output + * podmanv2 volume create + * Add stubs for cmd/podman in non-Linux local mode + * Make libpod/lock/shm completely Linux-only + * Add stubs for pkg/adapter/terminal_linux.go + * Add a stub for libpod.Container.Top + * Make cmd/podman/shared.GenerateCommand tests Linux-only + * Fix the libpod.LabelVolumePath stub + * Only run TestGetImageConfigStopSignal on Linux + * Fix the pkg/specgen/SpecGenerator.getSeccompConfig stub + * podmanv2 pod exists + * when removing networks for tests, force should be used + * Add basic structure of a spec generator for pods + * [CI:DOCS]fix type issue in pod binding test + * podmanv2 enable remote wait + * fix remote connection use of context + * use boolreport for containerexists response + * podmanv2 container exists|wait + * Add APIV2 service files + * Attempt manual removal of CNI IP allocations on refresh + * Implemented --iidfile for podman commit + * Add guildline for writing podman V2 CLI commands + * Use creds form PullImage remote + * Fix docker man page links + * Bump to v1.8.3-dev + * [CI:DOCS]remove podmanv2 binary + * Cirrus: Update VM images + * Cirrus-CI: Fix source path of vendor task + * Cirrus: Enable future installing buildah packages + * Cirrus: Include packages for buildah CI + * Cirrus: Update Ubuntu base images + * Cirrus: Use opensuse open build Ubuntu packages + * Update release notes for v1.8.2 final release + * rootlessport: handle SIGPIPE + * apiv2 add bindings for logs|events + * Bump github.com/containers/common from 0.5.0 to 0.6.1 + * Add inspect for exec sessions + * Add structure for new exec session tracking to DB + * Populate ExecSession with all required fields + * Fix path of tmp_dir + * Cirrus: Disable non-docs release processing + * container prune remove state created, configured + * Cirrus: Log libseccomp package version + * docs: mention that "podman version" prints out Remote API Version + - Changelog for v1.8.2 (2020-03-19) * fix reported compat issues * Don't include SUBDIR in windows.zip diff --git a/cmd/podman/service.go b/cmd/podman/service.go index 41ec499a1..0280b01a4 100644 --- a/cmd/podman/service.go +++ b/cmd/podman/service.go @@ -155,9 +155,7 @@ func runREST(r *libpod.Runtime, uri string, timeout time.Duration) error { } }() - err = server.Serve() - logrus.Debugf("%d/%d Active connections/Total connections\n", server.ActiveConnections, server.TotalConnections) - return err + return server.Serve() } func runVarlink(r *libpod.Runtime, uri string, timeout time.Duration, c *cliconfig.ServiceValues) error { diff --git a/cmd/podman/shared/create.go b/cmd/podman/shared/create.go index 68a36d967..94b1e63dc 100644 --- a/cmd/podman/shared/create.go +++ b/cmd/podman/shared/create.go @@ -783,10 +783,12 @@ func ParseCreateOpts(ctx context.Context, c *GenericCLIResults, runtime *libpod. Sysctl: sysctl, } + var securityOpt []string if c.Changed("security-opt") { - if err := secConfig.SetSecurityOpts(runtime, c.StringArray("security-opt")); err != nil { - return nil, err - } + securityOpt = c.StringArray("security-opt") + } + if err := secConfig.SetSecurityOpts(runtime, securityOpt); err != nil { + return nil, err } // SECCOMP diff --git a/cmd/podmanV2/containers/init.go b/cmd/podmanV2/containers/init.go new file mode 100644 index 000000000..dd1e1d21b --- /dev/null +++ b/cmd/podmanV2/containers/init.go @@ -0,0 +1,60 @@ +package containers + +import ( + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + initDescription = `Initialize one or more containers, creating the OCI spec and mounts for inspection. Container names or IDs can be used.` + + initCommand = &cobra.Command{ + Use: "init [flags] CONTAINER [CONTAINER...]", + Short: "Initialize one or more containers", + Long: initDescription, + PreRunE: preRunE, + RunE: initContainer, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, false) + }, + Example: `podman init --latest + podman init 3c45ef19d893 + podman init test1`, + } +) + +var ( + initOptions entities.ContainerInitOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: initCommand, + }) + flags := initCommand.Flags() + flags.BoolVarP(&initOptions.All, "all", "a", false, "Initialize all containers") + flags.BoolVarP(&initOptions.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + _ = flags.MarkHidden("latest") +} + +func initContainer(cmd *cobra.Command, args []string) error { + var errs utils.OutputErrors + report, err := registry.ContainerEngine().ContainerInit(registry.GetContext(), args, initOptions) + if err != nil { + return err + } + for _, r := range report { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/containers/kill.go b/cmd/podmanV2/containers/kill.go index 6e6debfec..3155d1f34 100644 --- a/cmd/podmanV2/containers/kill.go +++ b/cmd/podmanV2/containers/kill.go @@ -2,6 +2,7 @@ package containers import ( "context" + "errors" "fmt" "github.com/containers/libpod/cmd/podmanV2/parse" @@ -54,9 +55,13 @@ func kill(cmd *cobra.Command, args []string) error { ) // Check if the signalString provided by the user is valid // Invalid signals will return err - if _, err = signal.ParseSignalNameOrNumber(killOptions.Signal); err != nil { + sig, err := signal.ParseSignalNameOrNumber(killOptions.Signal) + if err != nil { return err } + if sig < 1 || sig > 64 { + return errors.New("valid signals are 1 through 64") + } responses, err := registry.ContainerEngine().ContainerKill(context.Background(), args, killOptions) if err != nil { return err diff --git a/cmd/podmanV2/containers/mount.go b/cmd/podmanV2/containers/mount.go new file mode 100644 index 000000000..4f7b95d98 --- /dev/null +++ b/cmd/podmanV2/containers/mount.go @@ -0,0 +1,124 @@ +package containers + +import ( + "encoding/json" + "fmt" + "os" + "text/tabwriter" + "text/template" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + mountDescription = `podman mount + Lists all mounted containers mount points if no container is specified + + podman mount CONTAINER-NAME-OR-ID + Mounts the specified container and outputs the mountpoint +` + + mountCommand = &cobra.Command{ + Use: "mount [flags] [CONTAINER]", + Short: "Mount a working container's root filesystem", + Long: mountDescription, + PreRunE: preRunE, + RunE: mount, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, true, false) + }, + Annotations: map[string]string{ + registry.RootRequired: "true", + }, + } +) + +var ( + mountOpts entities.ContainerMountOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode}, + Command: mountCommand, + }) + flags := mountCommand.Flags() + flags.BoolVarP(&mountOpts.All, "all", "a", false, "Mount all containers") + flags.StringVar(&mountOpts.Format, "format", "", "Change the output format to Go template") + flags.BoolVarP(&mountOpts.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + flags.BoolVar(&mountOpts.NoTruncate, "notruncate", false, "Do not truncate output") +} + +func mount(cmd *cobra.Command, args []string) error { + var ( + errs utils.OutputErrors + mrs []mountReporter + ) + reports, err := registry.ContainerEngine().ContainerMount(registry.GetContext(), args, mountOpts) + if err != nil { + return err + } + if len(args) > 0 || mountOpts.Latest || mountOpts.All { + for _, r := range reports { + if r.Err == nil { + fmt.Println(r.Path) + continue + } + errs = append(errs, r.Err) + } + return errs.PrintErrors() + } + if mountOpts.Format == "json" { + return printJSON(reports) + } + for _, r := range reports { + mrs = append(mrs, mountReporter{r}) + } + row := "{{.ID}} {{.Path}}" + format := "{{range . }}" + row + "{{end}}" + tmpl, err := template.New("mounts").Parse(format) + if err != nil { + return err + } + w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0) + defer w.Flush() + return tmpl.Execute(w, mrs) +} + +func printJSON(reports []*entities.ContainerMountReport) error { + type jreport struct { + ID string `json:"id"` + Names []string + Mountpoint string `json:"mountpoint"` + } + var jreports []jreport + + for _, r := range reports { + jreports = append(jreports, jreport{ + ID: r.Id, + Names: []string{r.Name}, + Mountpoint: r.Path, + }) + } + b, err := json.MarshalIndent(jreports, "", " ") + if err != nil { + return err + } + fmt.Println(string(b)) + return nil +} + +type mountReporter struct { + *entities.ContainerMountReport +} + +func (m mountReporter) ID() string { + if mountOpts.NoTruncate { + return m.Id + } + return m.Id[0:12] +} diff --git a/cmd/podmanV2/containers/ps.go b/cmd/podmanV2/containers/ps.go index 8c1d44842..8ebbf6ebf 100644 --- a/cmd/podmanV2/containers/ps.go +++ b/cmd/podmanV2/containers/ps.go @@ -4,6 +4,8 @@ import ( "encoding/json" "fmt" "os" + "sort" + "strconv" "strings" "text/tabwriter" "text/template" @@ -13,6 +15,8 @@ import ( "github.com/containers/buildah/pkg/formats" "github.com/containers/libpod/cmd/podmanV2/registry" "github.com/containers/libpod/pkg/domain/entities" + "github.com/cri-o/ocicni/pkg/ocicni" + "github.com/docker/go-units" "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -134,6 +138,7 @@ func getResponses() ([]entities.ListContainer, error) { } func ps(cmd *cobra.Command, args []string) error { + var responses []psReporter for _, f := range filters { split := strings.SplitN(f, "=", 2) if len(split) == 1 { @@ -141,22 +146,27 @@ func ps(cmd *cobra.Command, args []string) error { } listOpts.Filters[split[0]] = append(listOpts.Filters[split[0]], split[1]) } - responses, err := getResponses() + listContainers, err := getResponses() if err != nil { return err } if len(listOpts.Sort) > 0 { - responses, err = entities.SortPsOutput(listOpts.Sort, responses) + listContainers, err = entities.SortPsOutput(listOpts.Sort, listContainers) if err != nil { return err } } if listOpts.Format == "json" { - return jsonOut(responses) + return jsonOut(listContainers) } if listOpts.Quiet { - return quietOut(responses) + return quietOut(listContainers) + } + + for _, r := range listContainers { + responses = append(responses, psReporter{r}) } + headers, row := createPsOut() if cmd.Flag("format").Changed { row = listOpts.Format @@ -175,10 +185,14 @@ func ps(cmd *cobra.Command, args []string) error { w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0) if listOpts.Watch > 0 { for { + var responses []psReporter tm.Clear() tm.MoveCursor(1, 1) tm.Flush() - responses, err := getResponses() + listContainers, err := getResponses() + for _, r := range listContainers { + responses = append(responses, psReporter{r}) + } if err != nil { return err } @@ -210,21 +224,12 @@ func createPsOut() (string, string) { return headers, row } headers := defaultHeaders - if noTrunc { - row += "{{.ID}}" - } else { - row += "{{slice .ID 0 12}}" - } + row += "{{.ID}}" row += "\t{{.Image}}\t{{.Command}}\t{{.CreatedHuman}}\t{{.State}}\t{{.Ports}}\t{{.Names}}" if listOpts.Pod { headers += "\tPOD ID\tPODNAME" - if noTrunc { - row += "\t{{.Pod}}" - } else { - row += "\t{{slice .Pod 0 12}}" - } - row += "\t{{.PodName}}" + row += "\t{{.Pod}}\t{{.PodName}}" } if listOpts.Size { @@ -239,3 +244,171 @@ func createPsOut() (string, string) { } return headers, row } + +type psReporter struct { + entities.ListContainer +} + +// ID returns the ID of the container +func (l psReporter) ID() string { + if !noTrunc { + return l.ListContainer.ID[0:12] + } + return l.ListContainer.ID +} + +// Pod returns the ID of the pod the container +// belongs to and appropriately truncates the ID +func (l psReporter) Pod() string { + if !noTrunc && len(l.ListContainer.Pod) > 0 { + return l.ListContainer.Pod[0:12] + } + return l.ListContainer.Pod +} + +// State returns the container state in human duration +func (l psReporter) State() string { + var state string + switch l.ListContainer.State { + case "running": + t := units.HumanDuration(time.Since(time.Unix(l.StartedAt, 0))) + state = "Up " + t + " ago" + case "configured": + state = "Created" + case "exited", "stopped": + t := units.HumanDuration(time.Since(time.Unix(l.ExitedAt, 0))) + state = fmt.Sprintf("Exited (%d) %s ago", l.ExitCode, t) + default: + state = l.ListContainer.State + } + return state +} + +// Command returns the container command in string format +func (l psReporter) Command() string { + return strings.Join(l.ListContainer.Command, " ") +} + +// Size returns the rootfs and virtual sizes in human duration in +// and output form (string) suitable for ps +func (l psReporter) Size() string { + virt := units.HumanSizeWithPrecision(float64(l.ListContainer.Size.RootFsSize), 3) + s := units.HumanSizeWithPrecision(float64(l.ListContainer.Size.RwSize), 3) + return fmt.Sprintf("%s (virtual %s)", s, virt) +} + +// Names returns the container name in string format +func (l psReporter) Names() string { + return l.ListContainer.Names[0] +} + +// Ports converts from Portmappings to the string form +// required by ps +func (l psReporter) Ports() string { + if len(l.ListContainer.Ports) < 1 { + return "" + } + return portsToString(l.ListContainer.Ports) +} + +// CreatedAt returns the container creation time in string format. podman +// and docker both return a timestamped value for createdat +func (l psReporter) CreatedAt() string { + return time.Unix(l.Created, 0).String() +} + +// CreateHuman allows us to output the created time in human readable format +func (l psReporter) CreatedHuman() string { + return units.HumanDuration(time.Since(time.Unix(l.Created, 0))) + " ago" +} + +// portsToString converts the ports used to a string of the from "port1, port2" +// and also groups a continuous list of ports into a readable format. +func portsToString(ports []ocicni.PortMapping) string { + type portGroup struct { + first int32 + last int32 + } + var portDisplay []string + if len(ports) == 0 { + return "" + } + //Sort the ports, so grouping continuous ports become easy. + sort.Slice(ports, func(i, j int) bool { + return comparePorts(ports[i], ports[j]) + }) + + // portGroupMap is used for grouping continuous ports. + portGroupMap := make(map[string]*portGroup) + var groupKeyList []string + + for _, v := range ports { + + hostIP := v.HostIP + if hostIP == "" { + hostIP = "0.0.0.0" + } + // If hostPort and containerPort are not same, consider as individual port. + if v.ContainerPort != v.HostPort { + portDisplay = append(portDisplay, fmt.Sprintf("%s:%d->%d/%s", hostIP, v.HostPort, v.ContainerPort, v.Protocol)) + continue + } + + portMapKey := fmt.Sprintf("%s/%s", hostIP, v.Protocol) + + portgroup, ok := portGroupMap[portMapKey] + if !ok { + portGroupMap[portMapKey] = &portGroup{first: v.ContainerPort, last: v.ContainerPort} + // This list is required to traverse portGroupMap. + groupKeyList = append(groupKeyList, portMapKey) + continue + } + + if portgroup.last == (v.ContainerPort - 1) { + portgroup.last = v.ContainerPort + continue + } + } + // For each portMapKey, format group list and appned to output string. + for _, portKey := range groupKeyList { + group := portGroupMap[portKey] + portDisplay = append(portDisplay, formatGroup(portKey, group.first, group.last)) + } + return strings.Join(portDisplay, ", ") +} + +func comparePorts(i, j ocicni.PortMapping) bool { + if i.ContainerPort != j.ContainerPort { + return i.ContainerPort < j.ContainerPort + } + + if i.HostIP != j.HostIP { + return i.HostIP < j.HostIP + } + + if i.HostPort != j.HostPort { + return i.HostPort < j.HostPort + } + + return i.Protocol < j.Protocol +} + +// formatGroup returns the group as <IP:startPort:lastPort->startPort:lastPort/Proto> +// e.g 0.0.0.0:1000-1006->1000-1006/tcp. +func formatGroup(key string, start, last int32) string { + parts := strings.Split(key, "/") + groupType := parts[0] + var ip string + if len(parts) > 1 { + ip = parts[0] + groupType = parts[1] + } + group := strconv.Itoa(int(start)) + if start != last { + group = fmt.Sprintf("%s-%d", group, last) + } + if ip != "" { + group = fmt.Sprintf("%s:%s->%s", ip, group, group) + } + return fmt.Sprintf("%s/%s", group, groupType) +} diff --git a/cmd/podmanV2/containers/stop.go b/cmd/podmanV2/containers/stop.go index d6f31352f..53ec2934d 100644 --- a/cmd/podmanV2/containers/stop.go +++ b/cmd/podmanV2/containers/stop.go @@ -46,7 +46,7 @@ func init() { flags.StringArrayVarP(&stopOptions.CIDFiles, "cidfile", "", nil, "Read the container ID from the file") flags.BoolVarP(&stopOptions.Latest, "latest", "l", false, "Act on the latest container podman is aware of") flags.UintVarP(&stopTimeout, "time", "t", defaultContainerConfig.Engine.StopTimeout, "Seconds to wait for stop before killing the container") - if registry.EngineOptions.EngineMode == entities.ABIMode { + if registry.PodmanOptions.EngineMode == entities.ABIMode { _ = flags.MarkHidden("latest") _ = flags.MarkHidden("cidfile") _ = flags.MarkHidden("ignore") diff --git a/cmd/podmanV2/containers/unmount.go b/cmd/podmanV2/containers/unmount.go new file mode 100644 index 000000000..2a6ef14b0 --- /dev/null +++ b/cmd/podmanV2/containers/unmount.go @@ -0,0 +1,65 @@ +package containers + +import ( + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + description = `Container storage increments a mount counter each time a container is mounted. + + When a container is unmounted, the mount counter is decremented. The container's root filesystem is physically unmounted only when the mount counter reaches zero indicating no other processes are using the mount. + + An unmount can be forced with the --force flag. +` + umountCommand = &cobra.Command{ + Use: "umount [flags] CONTAINER [CONTAINER...]", + Aliases: []string{"unmount"}, + Short: "Unmounts working container's root filesystem", + Long: description, + RunE: unmount, + PreRunE: preRunE, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, false) + }, + Example: `podman umount ctrID + podman umount ctrID1 ctrID2 ctrID3 + podman umount --all`, + } +) + +var ( + unmountOpts entities.ContainerUnmountOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode}, + Command: umountCommand, + }) + flags := umountCommand.Flags() + flags.BoolVarP(&unmountOpts.All, "all", "a", false, "Umount all of the currently mounted containers") + flags.BoolVarP(&unmountOpts.Force, "force", "f", false, "Force the complete umount all of the currently mounted containers") + flags.BoolVarP(&unmountOpts.Latest, "latest", "l", false, "Act on the latest container podman is aware of") +} + +func unmount(cmd *cobra.Command, args []string) error { + var errs utils.OutputErrors + reports, err := registry.ContainerEngine().ContainerUnmount(registry.GetContext(), args, unmountOpts) + if err != nil { + return err + } + for _, r := range reports { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/containers/wait.go b/cmd/podmanV2/containers/wait.go index 29a0e5546..3d11c581e 100644 --- a/cmd/podmanV2/containers/wait.go +++ b/cmd/podmanV2/containers/wait.go @@ -44,7 +44,7 @@ func init() { flags.DurationVarP(&waitOptions.Interval, "interval", "i", time.Duration(250), "Milliseconds to wait before polling for completion") flags.BoolVarP(&waitOptions.Latest, "latest", "l", false, "Act on the latest container podman is aware of") flags.StringVar(&waitCondition, "condition", "stopped", "Condition to wait on") - if registry.EngineOptions.EngineMode == entities.ABIMode { + if registry.PodmanOptions.EngineMode == entities.ABIMode { // TODO: This is the same as V1. We could skip creating the flag altogether in V2... _ = flags.MarkHidden("latest") } @@ -70,7 +70,7 @@ func wait(cmd *cobra.Command, args []string) error { } for _, r := range responses { if r.Error == nil { - fmt.Println(r.Id) + fmt.Println(r.ExitCode) } else { errs = append(errs, r.Error) } diff --git a/cmd/podmanV2/images/history.go b/cmd/podmanV2/images/history.go index f6f15e2f2..e3bb7a051 100644 --- a/cmd/podmanV2/images/history.go +++ b/cmd/podmanV2/images/history.go @@ -8,10 +8,11 @@ import ( "text/tabwriter" "text/template" "time" + "unicode" "github.com/containers/libpod/cmd/podmanV2/registry" - "github.com/containers/libpod/cmd/podmanV2/report" "github.com/containers/libpod/pkg/domain/entities" + "github.com/docker/go-units" jsoniter "github.com/json-iterator/go" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -52,7 +53,7 @@ func init() { flags := historyCmd.Flags() flags.StringVar(&opts.format, "format", "", "Change the output to JSON or a Go template") - flags.BoolVarP(&opts.human, "human", "H", false, "Display sizes and dates in human readable format") + flags.BoolVarP(&opts.human, "human", "H", true, "Display sizes and dates in human readable format") flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate the output") flags.BoolVar(&opts.noTrunc, "notruncate", false, "Do not truncate the output") flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Display the numeric IDs only") @@ -78,7 +79,7 @@ func history(cmd *cobra.Command, args []string) error { layers := make([]layer, len(results.Layers)) for i, l := range results.Layers { layers[i].ImageHistoryLayer = l - layers[i].Created = time.Unix(l.Created, 0).Format(time.RFC3339) + layers[i].Created = l.Created.Format(time.RFC3339) } json := jsoniter.ConfigCompatibleWithStandardLibrary enc := json.NewEncoder(os.Stdout) @@ -86,10 +87,13 @@ func history(cmd *cobra.Command, args []string) error { } return err } - + var hr []historyreporter + for _, l := range results.Layers { + hr = append(hr, historyreporter{l}) + } // Defaults hdr := "ID\tCREATED\tCREATED BY\tSIZE\tCOMMENT\n" - row := "{{slice .ID 0 12}}\t{{humanDuration .Created}}\t{{ellipsis .CreatedBy 45}}\t{{.Size}}\t{{.Comment}}\n" + row := "{{.ID}}\t{{.Created}}\t{{.CreatedBy}}\t{{.Size}}\t{{.Comment}}\n" if len(opts.format) > 0 { hdr = "" @@ -100,9 +104,9 @@ func history(cmd *cobra.Command, args []string) error { } else { switch { case opts.human: - row = "{{slice .ID 0 12}}\t{{humanDuration .Created}}\t{{ellipsis .CreatedBy 45}}\t{{humanSize .Size}}\t{{.Comment}}\n" + row = "{{.ID}}\t{{.Created}}\t{{.CreatedBy}}\t{{.Size}}\t{{.Comment}}\n" case opts.noTrunc: - row = "{{.ID}}\t{{humanDuration .Created}}\t{{.CreatedBy}}\t{{humanSize .Size}}\t{{.Comment}}\n" + row = "{{.ID}}\t{{.Created}}\t{{.CreatedBy}}\t{{.Size}}\t{{.Comment}}\n" case opts.quiet: hdr = "" row = "{{.ID}}\n" @@ -110,14 +114,43 @@ func history(cmd *cobra.Command, args []string) error { } format := hdr + "{{range . }}" + row + "{{end}}" - tmpl := template.Must(template.New("report").Funcs(report.PodmanTemplateFuncs()).Parse(format)) + tmpl := template.Must(template.New("report").Parse(format)) w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0) - - _, _ = w.Write(report.ReportHeader("id", "created", "created by", "size", "comment")) - err = tmpl.Execute(w, results.Layers) + err = tmpl.Execute(w, hr) if err != nil { fmt.Fprintln(os.Stderr, errors.Wrapf(err, "Failed to print report")) } w.Flush() return nil } + +type historyreporter struct { + entities.ImageHistoryLayer +} + +func (h historyreporter) Created() string { + if opts.human { + return units.HumanDuration(time.Since(h.ImageHistoryLayer.Created)) + " ago" + } + return h.ImageHistoryLayer.Created.Format(time.RFC3339) +} + +func (h historyreporter) Size() string { + s := units.HumanSizeWithPrecision(float64(h.ImageHistoryLayer.Size), 3) + i := strings.LastIndexFunc(s, unicode.IsNumber) + return s[:i+1] + " " + s[i+1:] +} + +func (h historyreporter) CreatedBy() string { + if len(h.ImageHistoryLayer.CreatedBy) > 45 { + return h.ImageHistoryLayer.CreatedBy[:45-3] + "..." + } + return h.ImageHistoryLayer.CreatedBy +} + +func (h historyreporter) ID() string { + if !opts.noTrunc && len(h.ImageHistoryLayer.ID) >= 12 { + return h.ImageHistoryLayer.ID[0:12] + } + return h.ImageHistoryLayer.ID +} diff --git a/cmd/podmanV2/images/inspect.go b/cmd/podmanV2/images/inspect.go index d7f6b0ee1..2ee2d86ee 100644 --- a/cmd/podmanV2/images/inspect.go +++ b/cmd/podmanV2/images/inspect.go @@ -67,7 +67,6 @@ func inspect(cmd *cobra.Command, args []string) error { } return nil } - row := inspectFormat(inspectOpts.Format) format := "{{range . }}" + row + "{{end}}" tmpl, err := template.New("inspect").Parse(format) @@ -77,7 +76,7 @@ func inspect(cmd *cobra.Command, args []string) error { w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0) defer func() { _ = w.Flush() }() - err = tmpl.Execute(w, results) + err = tmpl.Execute(w, results.Images) if err != nil { return err } diff --git a/cmd/podmanV2/images/list.go b/cmd/podmanV2/images/list.go index 6b02e239e..d594a4323 100644 --- a/cmd/podmanV2/images/list.go +++ b/cmd/podmanV2/images/list.go @@ -9,10 +9,11 @@ import ( "text/tabwriter" "text/template" "time" + "unicode" "github.com/containers/libpod/cmd/podmanV2/registry" - "github.com/containers/libpod/cmd/podmanV2/report" "github.com/containers/libpod/pkg/domain/entities" + "github.com/docker/go-units" jsoniter "github.com/json-iterator/go" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -133,16 +134,10 @@ func writeTemplate(imageS []*entities.ImageSummary, err error) error { var ( hdr, row string ) - type image struct { - entities.ImageSummary - Repository string `json:"repository,omitempty"` - Tag string `json:"tag,omitempty"` - } - - imgs := make([]image, 0, len(imageS)) + imgs := make([]imageReporter, 0, len(imageS)) for _, e := range imageS { for _, tag := range e.RepoTags { - var h image + var h imageReporter h.ImageSummary = *e h.Repository, h.Tag = tokenRepoTag(tag) imgs = append(imgs, h) @@ -160,7 +155,7 @@ func writeTemplate(imageS []*entities.ImageSummary, err error) error { } } format := hdr + "{{range . }}" + row + "{{end}}" - tmpl := template.Must(template.New("list").Funcs(report.PodmanTemplateFuncs()).Parse(format)) + tmpl := template.Must(template.New("list").Parse(format)) w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0) defer w.Flush() return tmpl.Execute(w, imgs) @@ -208,7 +203,7 @@ func sortFunc(key string, data []*entities.ImageSummary) func(i, j int) bool { func imageListFormat(flags listFlagType) (string, string) { if flags.quiet { - return "", "{{slice .ID 0 12}}\n" + return "", "{{.ID}}\n" } // Defaults @@ -224,15 +219,15 @@ func imageListFormat(flags listFlagType) (string, string) { if flags.noTrunc { row += "\tsha256:{{.ID}}" } else { - row += "\t{{slice .ID 0 12}}" + row += "\t{{.ID}}" } hdr += "\tCREATED\tSIZE" - row += "\t{{humanDuration .Created}}\t{{humanSize .Size}}" + row += "\t{{.Created}}\t{{.Size}}" if flags.history { hdr += "\tHISTORY" - row += "\t{{if .History}}{{join .History \", \"}}{{else}}<none>{{end}}" + row += "\t{{if .History}}{{.History}}{{else}}<none>{{end}}" } if flags.readOnly { @@ -249,3 +244,30 @@ func imageListFormat(flags listFlagType) (string, string) { row += "\n" return hdr, row } + +type imageReporter struct { + Repository string `json:"repository,omitempty"` + Tag string `json:"tag,omitempty"` + entities.ImageSummary +} + +func (i imageReporter) ID() string { + if !listFlag.noTrunc && len(i.ImageSummary.ID) >= 12 { + return i.ImageSummary.ID[0:12] + } + return i.ImageSummary.ID +} + +func (i imageReporter) Created() string { + return units.HumanDuration(time.Since(time.Unix(i.ImageSummary.Created, 0))) + " ago" +} + +func (i imageReporter) Size() string { + s := units.HumanSizeWithPrecision(float64(i.ImageSummary.Size), 3) + j := strings.LastIndexFunc(s, unicode.IsNumber) + return s[:j+1] + " " + s[j+1:] +} + +func (i imageReporter) History() string { + return strings.Join(i.ImageSummary.History, ", ") +} diff --git a/cmd/podmanV2/images/load.go b/cmd/podmanV2/images/load.go index f60dc4908..315dbed3a 100644 --- a/cmd/podmanV2/images/load.go +++ b/cmd/podmanV2/images/load.go @@ -3,11 +3,18 @@ package images import ( "context" "fmt" + "io" + "io/ioutil" + "os" + "github.com/containers/image/v5/docker/reference" + "github.com/containers/libpod/cmd/podmanV2/parse" "github.com/containers/libpod/cmd/podmanV2/registry" - "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/util" + "github.com/pkg/errors" "github.com/spf13/cobra" + "golang.org/x/crypto/ssh/terminal" ) var ( @@ -46,11 +53,40 @@ func init() { func load(cmd *cobra.Command, args []string) error { if len(args) > 0 { - repo, err := image.NormalizedTag(args[0]) + ref, err := reference.Parse(args[0]) if err != nil { return err } - loadOpts.Name = repo.Name() + if t, ok := ref.(reference.Tagged); ok { + loadOpts.Tag = t.Tag() + } else { + loadOpts.Tag = "latest" + } + if r, ok := ref.(reference.Named); ok { + fmt.Println(r.Name()) + loadOpts.Name = r.Name() + } + } + if len(loadOpts.Input) > 0 { + if err := parse.ValidateFileName(loadOpts.Input); err != nil { + return err + } + } else { + if terminal.IsTerminal(int(os.Stdin.Fd())) { + return errors.Errorf("cannot read from terminal. Use command-line redirection or the --input flag.") + } + outFile, err := ioutil.TempFile(util.Tmpdir(), "podman") + if err != nil { + return errors.Errorf("error creating file %v", err) + } + defer os.Remove(outFile.Name()) + defer outFile.Close() + + _, err = io.Copy(outFile, os.Stdin) + if err != nil { + return errors.Errorf("error copying file %v", err) + } + loadOpts.Input = outFile.Name() } response, err := registry.ImageEngine().Load(context.Background(), loadOpts) if err != nil { diff --git a/cmd/podmanV2/images/rm.go b/cmd/podmanV2/images/rm.go index bb5880de3..6784182d9 100644 --- a/cmd/podmanV2/images/rm.go +++ b/cmd/podmanV2/images/rm.go @@ -8,6 +8,7 @@ import ( "github.com/containers/libpod/pkg/domain/entities" "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) var ( @@ -33,11 +34,13 @@ func init() { Parent: imageCmd, }) - flags := rmCmd.Flags() + imageRemoveFlagSet(rmCmd.Flags()) +} + +func imageRemoveFlagSet(flags *pflag.FlagSet) { flags.BoolVarP(&imageOpts.All, "all", "a", false, "Remove all images") flags.BoolVarP(&imageOpts.Force, "force", "f", false, "Force Removal of the image") } - func rm(cmd *cobra.Command, args []string) error { if len(args) < 1 && !imageOpts.All { @@ -46,7 +49,6 @@ func rm(cmd *cobra.Command, args []string) error { if len(args) > 0 && imageOpts.All { return errors.Errorf("when using the --all switch, you may not pass any images names or IDs") } - report, err := registry.ImageEngine().Delete(registry.GetContext(), args, imageOpts) if err != nil { switch { diff --git a/cmd/podmanV2/images/rmi.go b/cmd/podmanV2/images/rmi.go index 7f9297bc9..973763966 100644 --- a/cmd/podmanV2/images/rmi.go +++ b/cmd/podmanV2/images/rmi.go @@ -27,4 +27,5 @@ func init() { }) rmiCmd.SetHelpTemplate(registry.HelpTemplate()) rmiCmd.SetUsageTemplate(registry.UsageTemplate()) + imageRemoveFlagSet(rmiCmd.Flags()) } diff --git a/cmd/podmanV2/images/search.go b/cmd/podmanV2/images/search.go new file mode 100644 index 000000000..2ab9735ec --- /dev/null +++ b/cmd/podmanV2/images/search.go @@ -0,0 +1,157 @@ +package images + +import ( + "reflect" + "strings" + + buildahcli "github.com/containers/buildah/pkg/cli" + "github.com/containers/buildah/pkg/formats" + "github.com/containers/image/v5/types" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/util/camelcase" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// searchOptionsWrapper wraps entities.ImagePullOptions and prevents leaking +// CLI-only fields into the API types. +type searchOptionsWrapper struct { + entities.ImageSearchOptions + // CLI only flags + TLSVerifyCLI bool // Used to convert to an optional bool later + Format string // For go templating +} + +var ( + searchOptions = searchOptionsWrapper{} + searchDescription = `Search registries for a given image. Can search all the default registries or a specific registry. + + Users can limit the number of results, and filter the output based on certain conditions.` + + // Command: podman search + searchCmd = &cobra.Command{ + Use: "search [flags] TERM", + Short: "Search registry for image", + Long: searchDescription, + PreRunE: preRunE, + RunE: imageSearch, + Args: cobra.ExactArgs(1), + Example: `podman search --filter=is-official --limit 3 alpine + podman search registry.fedoraproject.org/ # only works with v2 registries + podman search --format "table {{.Index}} {{.Name}}" registry.fedoraproject.org/fedora`, + } + + // Command: podman image search + imageSearchCmd = &cobra.Command{ + Use: searchCmd.Use, + Short: searchCmd.Short, + Long: searchCmd.Long, + PreRunE: searchCmd.PreRunE, + RunE: searchCmd.RunE, + Args: searchCmd.Args, + Example: `podman image search --filter=is-official --limit 3 alpine + podman image search registry.fedoraproject.org/ # only works with v2 registries + podman image search --format "table {{.Index}} {{.Name}}" registry.fedoraproject.org/fedora`, + } +) + +func init() { + // search + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: searchCmd, + }) + + searchCmd.SetHelpTemplate(registry.HelpTemplate()) + searchCmd.SetUsageTemplate(registry.UsageTemplate()) + flags := searchCmd.Flags() + searchFlags(flags) + + // images search + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: imageSearchCmd, + Parent: imageCmd, + }) + + imageSearchFlags := imageSearchCmd.Flags() + searchFlags(imageSearchFlags) +} + +// searchFlags set the flags for the pull command. +func searchFlags(flags *pflag.FlagSet) { + flags.StringSliceVarP(&searchOptions.Filters, "filter", "f", []string{}, "Filter output based on conditions provided (default [])") + flags.StringVar(&searchOptions.Format, "format", "", "Change the output format to a Go template") + flags.IntVar(&searchOptions.Limit, "limit", 0, "Limit the number of results") + flags.BoolVar(&searchOptions.NoTrunc, "no-trunc", false, "Do not truncate the output") + flags.StringVar(&searchOptions.Authfile, "authfile", buildahcli.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") + flags.BoolVar(&searchOptions.TLSVerifyCLI, "tls-verify", true, "Require HTTPS and verify certificates when contacting registries") + if registry.IsRemote() { + _ = flags.MarkHidden("authfile") + _ = flags.MarkHidden("tls-verify") + } +} + +// imageSearch implements the command for searching images. +func imageSearch(cmd *cobra.Command, args []string) error { + searchTerm := "" + switch len(args) { + case 1: + searchTerm = args[0] + default: + return errors.Errorf("search requires exactly one argument") + } + + sarchOptsAPI := searchOptions.ImageSearchOptions + // TLS verification in c/image is controlled via a `types.OptionalBool` + // which allows for distinguishing among set-true, set-false, unspecified + // which is important to implement a sane way of dealing with defaults of + // boolean CLI flags. + if cmd.Flags().Changed("tls-verify") { + sarchOptsAPI.TLSVerify = types.NewOptionalBool(pullOptions.TLSVerifyCLI) + } + + searchReport, err := registry.ImageEngine().Search(registry.GetContext(), searchTerm, sarchOptsAPI) + if err != nil { + return err + } + + format := genSearchFormat(searchOptions.Format) + if len(searchReport) == 0 { + return nil + } + out := formats.StdoutTemplateArray{Output: searchToGeneric(searchReport), Template: format, Fields: searchHeaderMap()} + return out.Out() +} + +// searchHeaderMap returns the headers of a SearchResult. +func searchHeaderMap() map[string]string { + s := new(entities.ImageSearchReport) + v := reflect.Indirect(reflect.ValueOf(s)) + values := make(map[string]string, v.NumField()) + + for i := 0; i < v.NumField(); i++ { + key := v.Type().Field(i).Name + value := key + values[key] = strings.ToUpper(strings.Join(camelcase.Split(value), " ")) + } + return values +} + +func genSearchFormat(format string) string { + if format != "" { + // "\t" from the command line is not being recognized as a tab + // replacing the string "\t" to a tab character if the user passes in "\t" + return strings.Replace(format, `\t`, "\t", -1) + } + return "table {{.Index}}\t{{.Name}}\t{{.Description}}\t{{.Stars}}\t{{.Official}}\t{{.Automated}}\t" +} + +func searchToGeneric(params []entities.ImageSearchReport) (genericParams []interface{}) { + for _, v := range params { + genericParams = append(genericParams, interface{}(v)) + } + return genericParams +} diff --git a/cmd/podmanV2/images/tag.go b/cmd/podmanV2/images/tag.go index f66fe7857..f8799d4a7 100644 --- a/cmd/podmanV2/images/tag.go +++ b/cmd/podmanV2/images/tag.go @@ -9,11 +9,12 @@ import ( var ( tagDescription = "Adds one or more additional names to locally-stored image." tagCommand = &cobra.Command{ - Use: "tag [flags] IMAGE TARGET_NAME [TARGET_NAME...]", - Short: "Add an additional name to a local image", - Long: tagDescription, - RunE: tag, - Args: cobra.MinimumNArgs(2), + Use: "tag [flags] IMAGE TARGET_NAME [TARGET_NAME...]", + Short: "Add an additional name to a local image", + Long: tagDescription, + RunE: tag, + PreRunE: preRunE, + Args: cobra.MinimumNArgs(2), Example: `podman tag 0e3bbc2 fedora:latest podman tag imageID:latest myNewImage:newTag podman tag httpd myregistryhost:5000/fedora/httpd:v2`, diff --git a/cmd/podmanV2/main.go b/cmd/podmanV2/main.go index fe3cd9f16..cfe20d1c1 100644 --- a/cmd/podmanV2/main.go +++ b/cmd/podmanV2/main.go @@ -3,8 +3,6 @@ package main import ( "os" "reflect" - "runtime" - "strings" _ "github.com/containers/libpod/cmd/podmanV2/containers" _ "github.com/containers/libpod/cmd/podmanV2/healthcheck" @@ -14,36 +12,13 @@ import ( "github.com/containers/libpod/cmd/podmanV2/registry" _ "github.com/containers/libpod/cmd/podmanV2/system" _ "github.com/containers/libpod/cmd/podmanV2/volumes" - "github.com/containers/libpod/libpod" - "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/storage/pkg/reexec" - "github.com/sirupsen/logrus" ) func init() { - if err := libpod.SetXdgDirs(); err != nil { - logrus.Errorf(err.Error()) - os.Exit(1) - } - - switch runtime.GOOS { - case "darwin": - fallthrough - case "windows": - registry.EngineOptions.EngineMode = entities.TunnelMode - case "linux": - registry.EngineOptions.EngineMode = entities.ABIMode - default: - logrus.Errorf("%s is not a supported OS", runtime.GOOS) - os.Exit(1) - } - - // TODO: Is there a Cobra way to "peek" at os.Args? - for _, v := range os.Args { - if strings.HasPrefix(v, "--remote") { - registry.EngineOptions.EngineMode = entities.TunnelMode - } - } + // This is the bootstrap configuration, if user gives + // CLI flags parts of this configuration may be overwritten + registry.PodmanOptions = registry.NewPodmanConfig() } func main() { @@ -53,7 +28,7 @@ func main() { return } for _, c := range registry.Commands { - if Contains(registry.EngineOptions.EngineMode, c.Mode) { + if Contains(registry.PodmanOptions.EngineMode, c.Mode) { parent := rootCmd if c.Parent != nil { parent = c.Parent diff --git a/cmd/podmanV2/pods/create.go b/cmd/podmanV2/pods/create.go index ab8957ee3..2aaf0cd2c 100644 --- a/cmd/podmanV2/pods/create.go +++ b/cmd/podmanV2/pods/create.go @@ -123,6 +123,21 @@ func create(cmd *cobra.Command, args []string) error { } } + if !createOptions.Infra { + if cmd.Flag("infra-command").Changed { + return errors.New("cannot set infra-command without an infra container") + } + createOptions.InfraCommand = "" + if cmd.Flag("infra-image").Changed { + return errors.New("cannot set infra-image without an infra container") + } + createOptions.InfraImage = "" + if cmd.Flag("share").Changed { + return errors.New("cannot set share namespaces without an infra container") + } + createOptions.Share = nil + } + response, err := registry.ContainerEngine().PodCreate(context.Background(), createOptions) if err != nil { return err diff --git a/cmd/podmanV2/pods/pod.go b/cmd/podmanV2/pods/pod.go index 3766893bb..81c0d33e1 100644 --- a/cmd/podmanV2/pods/pod.go +++ b/cmd/podmanV2/pods/pod.go @@ -1,9 +1,6 @@ package pods import ( - "strings" - "text/template" - "github.com/containers/libpod/cmd/podmanV2/registry" "github.com/containers/libpod/pkg/domain/entities" "github.com/spf13/cobra" @@ -21,33 +18,6 @@ var ( } ) -var podFuncMap = template.FuncMap{ - "numCons": func(cons []*entities.ListPodContainer) int { - return len(cons) - }, - "podcids": func(cons []*entities.ListPodContainer) string { - var ctrids []string - for _, c := range cons { - ctrids = append(ctrids, c.Id[:12]) - } - return strings.Join(ctrids, ",") - }, - "podconnames": func(cons []*entities.ListPodContainer) string { - var ctrNames []string - for _, c := range cons { - ctrNames = append(ctrNames, c.Names[:12]) - } - return strings.Join(ctrNames, ",") - }, - "podconstatuses": func(cons []*entities.ListPodContainer) string { - var statuses []string - for _, c := range cons { - statuses = append(statuses, c.Status) - } - return strings.Join(statuses, ",") - }, -} - func init() { registry.Commands = append(registry.Commands, registry.CliCommand{ Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, diff --git a/cmd/podmanV2/pods/ps.go b/cmd/podmanV2/pods/ps.go index 9875263ac..3dac607df 100644 --- a/cmd/podmanV2/pods/ps.go +++ b/cmd/podmanV2/pods/ps.go @@ -9,9 +9,11 @@ import ( "strings" "text/tabwriter" "text/template" + "time" + + "github.com/docker/go-units" "github.com/containers/libpod/cmd/podmanV2/registry" - "github.com/containers/libpod/cmd/podmanV2/report" "github.com/containers/libpod/pkg/domain/entities" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -65,6 +67,7 @@ func pods(cmd *cobra.Command, args []string) error { var ( w io.Writer = os.Stdout row string + lpr []ListPodReporter ) if cmd.Flag("filter").Changed { for _, f := range strings.Split(inputFilters, ",") { @@ -88,6 +91,10 @@ func pods(cmd *cobra.Command, args []string) error { fmt.Println(string(b)) return nil } + + for _, r := range responses { + lpr = append(lpr, ListPodReporter{r}) + } headers, row := createPodPsOut() if psInput.Quiet { if noTrunc { @@ -106,15 +113,14 @@ func pods(cmd *cobra.Command, args []string) error { if !psInput.Quiet && !cmd.Flag("format").Changed { format = headers + format } - funcs := report.AppendFuncMap(podFuncMap) - tmpl, err := template.New("listPods").Funcs(funcs).Parse(format) + tmpl, err := template.New("listPods").Parse(format) if err != nil { return err } if !psInput.Quiet { w = tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0) } - if err := tmpl.Execute(w, responses); err != nil { + if err := tmpl.Execute(w, lpr); err != nil { return err } if flusher, ok := w.(interface{ Flush() error }); ok { @@ -132,20 +138,19 @@ func createPodPsOut() (string, string) { row += "{{slice .Id 0 12}}" } - row += "\t{{.Name}}\t{{.Status}}\t{{humanDurationFromTime .Created}}" + row += "\t{{.Name}}\t{{.Status}}\t{{.Created}}" - //rowFormat string = "{{slice .Id 0 12}}\t{{.Name}}\t{{.Status}}\t{{humanDurationFromTime .Created}}" if psInput.CtrIds { headers += "\tIDS" - row += "\t{{podcids .Containers}}" + row += "\t{{.ContainerIds}}" } if psInput.CtrNames { headers += "\tNAMES" - row += "\t{{podconnames .Containers}}" + row += "\t{{.ContainerNames}}" } if psInput.CtrStatus { headers += "\tSTATUS" - row += "\t{{podconstatuses .Containers}}" + row += "\t{{.ContainerStatuses}}" } if psInput.Namespace { headers += "\tCGROUP\tNAMESPACES" @@ -153,7 +158,7 @@ func createPodPsOut() (string, string) { } if !psInput.CtrStatus && !psInput.CtrNames && !psInput.CtrIds { headers += "\t# OF CONTAINERS" - row += "\t{{numCons .Containers}}" + row += "\t{{.NumberOfContainers}}" } headers += "\tINFRA ID\n" @@ -164,3 +169,61 @@ func createPodPsOut() (string, string) { } return headers, row } + +// ListPodReporter is a struct for pod ps output +type ListPodReporter struct { + *entities.ListPodsReport +} + +// Created returns a human readable created time/date +func (l ListPodReporter) Created() string { + return units.HumanDuration(time.Since(l.ListPodsReport.Created)) + " ago" +} + +// NumberofContainers returns an int representation for +// the number of containers belonging to the pod +func (l ListPodReporter) NumberOfContainers() int { + return len(l.Containers) +} + +// Added for backwards compatibility with podmanv1 +func (l ListPodReporter) InfraID() string { + return l.InfraId() +} + +// InfraId returns the infra container id for the pod +// depending on trunc +func (l ListPodReporter) InfraId() string { + if noTrunc { + return l.ListPodsReport.InfraId + } + return l.ListPodsReport.InfraId[0:12] +} + +func (l ListPodReporter) ContainerIds() string { + var ctrids []string + for _, c := range l.Containers { + id := c.Id + if !noTrunc { + id = id[0:12] + } + ctrids = append(ctrids, id) + } + return strings.Join(ctrids, ",") +} + +func (l ListPodReporter) ContainerNames() string { + var ctrNames []string + for _, c := range l.Containers { + ctrNames = append(ctrNames, c.Names) + } + return strings.Join(ctrNames, ",") +} + +func (l ListPodReporter) ContainerStatuses() string { + var statuses []string + for _, c := range l.Containers { + statuses = append(statuses, c.Status) + } + return strings.Join(statuses, ",") +} diff --git a/cmd/podmanV2/registry/config.go b/cmd/podmanV2/registry/config.go new file mode 100644 index 000000000..e68009a50 --- /dev/null +++ b/cmd/podmanV2/registry/config.go @@ -0,0 +1,59 @@ +package registry + +import ( + "os" + "runtime" + "strings" + + "github.com/containers/common/pkg/config" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/sirupsen/logrus" +) + +const ( + RootRequired = "RootRequired" +) + +var ( + PodmanOptions entities.PodmanConfig +) + +// NewPodmanConfig creates a PodmanConfig from the environment +func NewPodmanConfig() entities.PodmanConfig { + if err := libpod.SetXdgDirs(); err != nil { + logrus.Errorf(err.Error()) + os.Exit(1) + } + + var mode entities.EngineMode + switch runtime.GOOS { + case "darwin": + fallthrough + case "windows": + mode = entities.TunnelMode + case "linux": + mode = entities.ABIMode + default: + logrus.Errorf("%s is not a supported OS", runtime.GOOS) + os.Exit(1) + } + + // cobra.Execute() may not be called yet, so we peek at os.Args. + for _, v := range os.Args { + // Prefix checking works because of how default EngineMode's + // have been defined. + if strings.HasPrefix(v, "--remote=") { + mode = entities.TunnelMode + } + } + + // FIXME: for rootless, where to get the path + // TODO: + cfg, err := config.NewConfig("") + if err != nil { + logrus.Error("Failed to obtain podman configuration") + os.Exit(1) + } + return entities.PodmanConfig{Config: cfg, EngineMode: mode} +} diff --git a/cmd/podmanV2/registry/registry.go b/cmd/podmanV2/registry/registry.go index 07c2b33ff..5ef6a10d8 100644 --- a/cmd/podmanV2/registry/registry.go +++ b/cmd/podmanV2/registry/registry.go @@ -29,8 +29,9 @@ var ( exitCode = ExecErrorCodeGeneric imageEngine entities.ImageEngine - Commands []CliCommand - EngineOptions entities.EngineOptions + // Commands holds the cobra.Commands to present to the user, including + // parent if not a child of "root" + Commands []CliCommand ) func SetExitCode(code int) { @@ -83,8 +84,8 @@ func ImageEngine() entities.ImageEngine { // NewImageEngine is a wrapper for building an ImageEngine to be used for PreRunE functions func NewImageEngine(cmd *cobra.Command, args []string) (entities.ImageEngine, error) { if imageEngine == nil { - EngineOptions.FlagSet = cmd.Flags() - engine, err := infra.NewImageEngine(EngineOptions) + PodmanOptions.FlagSet = cmd.Flags() + engine, err := infra.NewImageEngine(PodmanOptions) if err != nil { return nil, err } @@ -100,8 +101,8 @@ func ContainerEngine() entities.ContainerEngine { // NewContainerEngine is a wrapper for building an ContainerEngine to be used for PreRunE functions func NewContainerEngine(cmd *cobra.Command, args []string) (entities.ContainerEngine, error) { if containerEngine == nil { - EngineOptions.FlagSet = cmd.Flags() - engine, err := infra.NewContainerEngine(EngineOptions) + PodmanOptions.FlagSet = cmd.Flags() + engine, err := infra.NewContainerEngine(PodmanOptions) if err != nil { return nil, err } @@ -125,24 +126,17 @@ func IdOrLatestArgs(cmd *cobra.Command, args []string) error { return nil } -type podmanContextKey string - -var podmanFactsKey = podmanContextKey("engineOptions") - -func NewOptions(ctx context.Context, facts *entities.EngineOptions) context.Context { - return context.WithValue(ctx, podmanFactsKey, facts) -} - -func Options(cmd *cobra.Command) (*entities.EngineOptions, error) { - if f, ok := cmd.Context().Value(podmanFactsKey).(*entities.EngineOptions); ok { - return f, errors.New("Command Context ") - } - return nil, nil -} - func GetContext() context.Context { if cliCtx == nil { - cliCtx = context.TODO() + cliCtx = context.Background() } return cliCtx } + +type ContextOptionsKey string + +const PodmanOptionsKey ContextOptionsKey = "PodmanOptions" + +func GetContextWithOptions() context.Context { + return context.WithValue(GetContext(), PodmanOptionsKey, PodmanOptions) +} diff --git a/cmd/podmanV2/registry/remote.go b/cmd/podmanV2/registry/remote.go index 32a231ac4..5378701e7 100644 --- a/cmd/podmanV2/registry/remote.go +++ b/cmd/podmanV2/registry/remote.go @@ -5,5 +5,5 @@ import ( ) func IsRemote() bool { - return EngineOptions.EngineMode == entities.TunnelMode + return PodmanOptions.EngineMode == entities.TunnelMode } diff --git a/cmd/podmanV2/report/templates.go b/cmd/podmanV2/report/templates.go deleted file mode 100644 index e46048e97..000000000 --- a/cmd/podmanV2/report/templates.go +++ /dev/null @@ -1,73 +0,0 @@ -package report - -import ( - "strings" - "text/template" - "time" - "unicode" - - "github.com/docker/go-units" -) - -var defaultFuncMap = template.FuncMap{ - "ellipsis": func(s string, length int) string { - if len(s) > length { - return s[:length-3] + "..." - } - return s - }, - "humanDuration": func(t int64) string { - return units.HumanDuration(time.Since(time.Unix(t, 0))) + " ago" - }, - "humanDurationFromTime": func(t time.Time) string { - return units.HumanDuration(time.Since(t)) + " ago" - }, - "humanSize": func(sz int64) string { - s := units.HumanSizeWithPrecision(float64(sz), 3) - i := strings.LastIndexFunc(s, unicode.IsNumber) - return s[:i+1] + " " + s[i+1:] - }, - "join": strings.Join, - "lower": strings.ToLower, - "rfc3339": func(t int64) string { - return time.Unix(t, 0).Format(time.RFC3339) - }, - "replace": strings.Replace, - "split": strings.Split, - "title": strings.Title, - "upper": strings.ToUpper, - // TODO: Remove after Go 1.14 port - "slice": func(s string, i, j int) string { - if i > j || len(s) < i { - return s - } - if len(s) < j { - return s[i:] - } - return s[i:j] - }, -} - -func ReportHeader(columns ...string) []byte { - hdr := make([]string, len(columns)) - for i, h := range columns { - hdr[i] = strings.ToUpper(h) - } - return []byte(strings.Join(hdr, "\t") + "\n") -} - -func AppendFuncMap(funcMap template.FuncMap) template.FuncMap { - merged := PodmanTemplateFuncs() - for k, v := range funcMap { - merged[k] = v - } - return merged -} - -func PodmanTemplateFuncs() template.FuncMap { - merged := make(template.FuncMap) - for k, v := range defaultFuncMap { - merged[k] = v - } - return merged -} diff --git a/cmd/podmanV2/root.go b/cmd/podmanV2/root.go index 6fc12f57e..0639257ea 100644 --- a/cmd/podmanV2/root.go +++ b/cmd/podmanV2/root.go @@ -1,29 +1,37 @@ package main import ( + "context" "fmt" "log/syslog" "os" "path" + "runtime/pprof" "github.com/containers/libpod/cmd/podmanV2/registry" "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/rootless" + "github.com/containers/libpod/pkg/tracing" "github.com/containers/libpod/version" + "github.com/opentracing/opentracing-go" + "github.com/pkg/errors" "github.com/sirupsen/logrus" logrusSyslog "github.com/sirupsen/logrus/hooks/syslog" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) var ( rootCmd = &cobra.Command{ - Use: path.Base(os.Args[0]), - Long: "Manage pods, containers and images", - SilenceUsage: true, - SilenceErrors: true, - TraverseChildren: true, - PersistentPreRunE: preRunE, - RunE: registry.SubCommandExists, - Version: version.Version, + Use: path.Base(os.Args[0]), + Long: "Manage pods, containers and images", + SilenceUsage: true, + SilenceErrors: true, + TraverseChildren: true, + PersistentPreRunE: preRunE, + RunE: registry.SubCommandExists, + PersistentPostRunE: postRunE, + Version: version.Version, } logLevels = entities.NewStringSet("debug", "info", "warn", "error", "fatal", "panic") @@ -32,30 +40,73 @@ var ( ) func init() { - // Override default --help information of `--version` global flag} - var dummyVersion bool - // TODO had to disable shorthand -v for version due to -v rm with volume - rootCmd.PersistentFlags().BoolVar(&dummyVersion, "version", false, "Version of Podman") - rootCmd.PersistentFlags().StringVarP(®istry.EngineOptions.Uri, "remote", "r", "", "URL to access Podman service") - rootCmd.PersistentFlags().StringSliceVar(®istry.EngineOptions.Identities, "identity", []string{}, "path to SSH identity file") - rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "error", fmt.Sprintf("Log messages above specified level (%s)", logLevels.String())) - rootCmd.PersistentFlags().BoolVar(&useSyslog, "syslog", false, "Output logging information to syslog as well as the console (default false)") - cobra.OnInitialize( - logging, + rootlessHook, + loggingHook, syslogHook, ) + + rootFlags(registry.PodmanOptions, rootCmd.PersistentFlags()) +} + +func Execute() { + if err := rootCmd.ExecuteContext(registry.GetContextWithOptions()); err != nil { + logrus.Error(err) + } else if registry.GetExitCode() == registry.ExecErrorCodeGeneric { + // The exitCode modified from registry.ExecErrorCodeGeneric, + // indicates an application + // running inside of a container failed, as opposed to the + // podman command failed. Must exit with that exit code + // otherwise command exited correctly. + registry.SetExitCode(0) + } + os.Exit(registry.GetExitCode()) } -func preRunE(cmd *cobra.Command, args []string) error { +func preRunE(cmd *cobra.Command, _ []string) error { + // Update PodmanOptions now that we "know" more + // TODO: pass in path overriding configuration file + registry.PodmanOptions = registry.NewPodmanConfig() + cmd.SetHelpTemplate(registry.HelpTemplate()) cmd.SetUsageTemplate(registry.UsageTemplate()) + + if cmd.Flag("cpu-profile").Changed { + f, err := os.Create(registry.PodmanOptions.CpuProfile) + if err != nil { + return errors.Wrapf(err, "unable to create cpu profiling file %s", + registry.PodmanOptions.CpuProfile) + } + if err := pprof.StartCPUProfile(f); err != nil { + return err + } + } + + if cmd.Flag("trace").Changed { + tracer, closer := tracing.Init("podman") + opentracing.SetGlobalTracer(tracer) + registry.PodmanOptions.SpanCloser = closer + + registry.PodmanOptions.Span = tracer.StartSpan("before-context") + registry.PodmanOptions.SpanCtx = opentracing.ContextWithSpan(context.Background(), registry.PodmanOptions.Span) + } return nil } -func logging() { +func postRunE(cmd *cobra.Command, args []string) error { + if cmd.Flag("cpu-profile").Changed { + pprof.StopCPUProfile() + } + if cmd.Flag("trace").Changed { + registry.PodmanOptions.Span.Finish() + registry.PodmanOptions.SpanCloser.Close() + } + return nil +} + +func loggingHook() { if !logLevels.Contains(logLevel) { - fmt.Fprintf(os.Stderr, "Log Level \"%s\" is not supported, choose from: %s\n", logLevel, logLevels.String()) + logrus.Errorf("Log Level \"%s\" is not supported, choose from: %s", logLevel, logLevels.String()) os.Exit(1) } @@ -83,17 +134,68 @@ func syslogHook() { } } -func Execute() { - o := registry.NewOptions(rootCmd.Context(), ®istry.EngineOptions) - if err := rootCmd.ExecuteContext(o); err != nil { - fmt.Fprintln(os.Stderr, "Error:", err.Error()) - } else if registry.GetExitCode() == registry.ExecErrorCodeGeneric { - // The exitCode modified from registry.ExecErrorCodeGeneric, - // indicates an application - // running inside of a container failed, as opposed to the - // podman command failed. Must exit with that exit code - // otherwise command exited correctly. - registry.SetExitCode(0) +func rootlessHook() { + if rootless.IsRootless() { + logrus.Error("rootless mode is currently not supported. Support will return ASAP.") } - os.Exit(registry.GetExitCode()) + // ce, err := registry.NewContainerEngine(rootCmd, []string{}) + // if err != nil { + // logrus.WithError(err).Fatal("failed to obtain container engine") + // } + // ce.SetupRootLess(rootCmd) +} + +func rootFlags(opts entities.PodmanConfig, flags *pflag.FlagSet) { + // V2 flags + flags.StringVarP(&opts.Uri, "remote", "r", "", "URL to access Podman service") + flags.StringSliceVar(&opts.Identities, "identity", []string{}, "path to SSH identity file") + + // Override default --help information of `--version` global flag + // TODO: restore -v option for version without breaking -v for volumes + var dummyVersion bool + flags.BoolVar(&dummyVersion, "version", false, "Version of Podman") + + cfg := opts.Config + flags.StringVar(&cfg.Engine.CgroupManager, "cgroup-manager", cfg.Engine.CgroupManager, opts.CGroupUsage) + flags.StringVar(&opts.CpuProfile, "cpu-profile", "", "Path for the cpu profiling results") + flags.StringVar(&opts.ConmonPath, "conmon", "", "Path of the conmon binary") + flags.StringVar(&cfg.Engine.NetworkCmdPath, "network-cmd-path", cfg.Engine.NetworkCmdPath, "Path to the command for configuring the network") + flags.StringVar(&cfg.Network.NetworkConfigDir, "cni-config-dir", cfg.Network.NetworkConfigDir, "Path of the configuration directory for CNI networks") + flags.StringVar(&cfg.Containers.DefaultMountsFile, "default-mounts-file", cfg.Containers.DefaultMountsFile, "Path to default mounts file") + flags.StringVar(&cfg.Engine.EventsLogger, "events-backend", cfg.Engine.EventsLogger, `Events backend to use ("file"|"journald"|"none")`) + flags.StringSliceVar(&cfg.Engine.HooksDir, "hooks-dir", cfg.Engine.HooksDir, "Set the OCI hooks directory path (may be set multiple times)") + flags.IntVar(&opts.MaxWorks, "max-workers", 0, "The maximum number of workers for parallel operations") + flags.StringVar(&cfg.Engine.Namespace, "namespace", cfg.Engine.Namespace, "Set the libpod namespace, used to create separate views of the containers and pods on the system") + flags.StringVar(&cfg.Engine.StaticDir, "root", "", "Path to the root directory in which data, including images, is stored") + flags.StringVar(&opts.Runroot, "runroot", "", "Path to the 'run directory' where all state information is stored") + flags.StringVar(&opts.RuntimePath, "runtime", "", "Path to the OCI-compatible binary used to run containers, default is /usr/bin/runc") + // -s is deprecated due to conflict with -s on subcommands + flags.StringVar(&opts.StorageDriver, "storage-driver", "", "Select which storage driver is used to manage storage of images and containers (default is overlay)") + flags.StringArrayVar(&opts.StorageOpts, "storage-opt", []string{}, "Used to pass an option to the storage driver") + + flags.StringVar(&opts.Engine.TmpDir, "tmpdir", "", "Path to the tmp directory for libpod state content.\n\nNote: use the environment variable 'TMPDIR' to change the temporary storage location for container images, '/var/tmp'.\n") + flags.BoolVar(&opts.Trace, "trace", false, "Enable opentracing output (default false)") + + // Override default --help information of `--help` global flag + var dummyHelp bool + flags.BoolVar(&dummyHelp, "help", false, "Help for podman") + flags.StringVar(&logLevel, "log-level", logLevel, fmt.Sprintf("Log messages above specified level (%s)", logLevels.String())) + + // Hide these flags for both ABI and Tunneling + for _, f := range []string{ + "cpu-profile", + "default-mounts-file", + "max-workers", + "trace", + } { + if err := flags.MarkHidden(f); err != nil { + logrus.Warnf("unable to mark %s flag as hidden", f) + } + } + + // Only create these flags for ABI connections + if !registry.IsRemote() { + flags.BoolVar(&useSyslog, "syslog", false, "Output logging information to syslog as well as the console (default false)") + } + } diff --git a/cmd/podmanV2/system/events.go b/cmd/podmanV2/system/events.go new file mode 100644 index 000000000..9fd27e2c1 --- /dev/null +++ b/cmd/podmanV2/system/events.go @@ -0,0 +1,104 @@ +package system + +import ( + "bufio" + "context" + "html/template" + "os" + + "github.com/containers/buildah/pkg/formats" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/libpod/events" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + eventsDescription = "Monitor podman events" + eventsCommand = &cobra.Command{ + Use: "events", + Args: cobra.NoArgs, + Short: "Show podman events", + Long: eventsDescription, + PersistentPreRunE: preRunE, + RunE: eventsCmd, + Example: `podman events + podman events --filter event=create + podman events --since 1h30s`, + } +) + +var ( + eventOptions entities.EventsOptions + eventFormat string +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: eventsCommand, + }) + flags := eventsCommand.Flags() + flags.StringArrayVar(&eventOptions.Filter, "filter", []string{}, "filter output") + flags.StringVar(&eventFormat, "format", "", "format the output using a Go template") + flags.BoolVar(&eventOptions.Stream, "stream", true, "stream new events; for testing only") + flags.StringVar(&eventOptions.Since, "since", "", "show all events created since timestamp") + flags.StringVar(&eventOptions.Until, "until", "", "show all events until timestamp") + _ = flags.MarkHidden("stream") +} + +func eventsCmd(cmd *cobra.Command, args []string) error { + var ( + err error + eventsError error + tmpl *template.Template + ) + if eventFormat != formats.JSONString { + tmpl, err = template.New("events").Parse(eventFormat) + if err != nil { + return err + } + } + if len(eventOptions.Since) > 0 || len(eventOptions.Until) > 0 { + eventOptions.FromStart = true + } + eventChannel := make(chan *events.Event) + eventOptions.EventChan = eventChannel + + go func() { + eventsError = registry.ContainerEngine().Events(context.Background(), eventOptions) + }() + if eventsError != nil { + return eventsError + } + + w := bufio.NewWriter(os.Stdout) + for event := range eventChannel { + switch { + case eventFormat == formats.JSONString: + jsonStr, err := event.ToJSONString() + if err != nil { + return errors.Wrapf(err, "unable to format json") + } + if _, err := w.Write([]byte(jsonStr)); err != nil { + return err + } + case len(eventFormat) > 0: + if err := tmpl.Execute(w, event); err != nil { + return err + } + default: + if _, err := w.Write([]byte(event.ToHumanReadable())); err != nil { + return err + } + } + if _, err := w.Write([]byte("\n")); err != nil { + return err + } + if err := w.Flush(); err != nil { + return err + } + } + return nil +} diff --git a/cmd/podmanV2/system/version.go b/cmd/podmanV2/system/version.go index e8002056b..8d6e8b7a8 100644 --- a/cmd/podmanV2/system/version.go +++ b/cmd/podmanV2/system/version.go @@ -24,7 +24,7 @@ var ( RunE: version, PersistentPreRunE: preRunE, } - format string + versionFormat string ) type versionStruct struct { @@ -38,7 +38,7 @@ func init() { Command: versionCommand, }) flags := versionCommand.Flags() - flags.StringVarP(&format, "format", "f", "", "Change the output format to JSON or a Go template") + flags.StringVarP(&versionFormat, "format", "f", "", "Change the output format to JSON or a Go template") } func version(cmd *cobra.Command, args []string) error { @@ -62,7 +62,7 @@ func version(cmd *cobra.Command, args []string) error { v.Server = v.Client //} - versionOutputFormat := format + versionOutputFormat := versionFormat if versionOutputFormat != "" { if strings.Join(strings.Fields(versionOutputFormat), "") == "{{json.}}" { versionOutputFormat = formats.JSONString diff --git a/contrib/cirrus/logformatter b/contrib/cirrus/logformatter index 6a86f6a49..738d2e19d 100755 --- a/contrib/cirrus/logformatter +++ b/contrib/cirrus/logformatter @@ -220,8 +220,8 @@ END_HTML $cirrus_task = $1; } - # BATS handling - if ($line =~ /^1\.\.\d+$/) { + # BATS handling (used also for apiv2 tests, which emit TAP output) + if ($line =~ /^1\.\.\d+$/ || $line =~ m!/test-apiv2!) { $looks_like_bats = 1; } if ($looks_like_bats) { @@ -234,6 +234,10 @@ END_HTML elsif ($line =~ /^#\s/) { $css = 'log' } if ($css) { + # Make it linkable, e.g. foo.html#t--00001 + if ($line =~ /^(not\s+)?ok\s+(\d+)/) { + $line = sprintf("<a name='t--%05d'>%s</a>", $2, $line); + } $line = "<span class='bats-$css'>$line</span>"; } diff --git a/contrib/spec/podman.spec.in b/contrib/spec/podman.spec.in index 9641a52e6..afc50f854 100644 --- a/contrib/spec/podman.spec.in +++ b/contrib/spec/podman.spec.in @@ -48,7 +48,7 @@ Epoch: 99 %else Epoch: 0 %endif -Version: 1.8.3 +Version: 2.0.0 Release: #COMMITDATE#.git%{shortcommit0}%{?dist} Summary: Manage Pods, Containers and Container Images License: ASL 2.0 diff --git a/libpod/container_api.go b/libpod/container_api.go index 55c79fa74..b31079b26 100644 --- a/libpod/container_api.go +++ b/libpod/container_api.go @@ -6,10 +6,13 @@ import ( "io/ioutil" "net" "os" + "strings" + "sync" "time" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/events" + "github.com/containers/libpod/libpod/logs" "github.com/opentracing/opentracing-go" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -244,15 +247,28 @@ func (c *Container) Attach(streams *define.AttachStreams, keys string, resize <- // forwarded to the client. // This function returns when the attach finishes. It does not hold the lock for // the duration of its runtime, only using it at the beginning to verify state. -func (c *Container) HTTPAttach(httpCon net.Conn, httpBuf *bufio.ReadWriter, streams *HTTPAttachStreams, detachKeys *string, cancel <-chan bool) error { +// The streamLogs parameter indicates that all the container's logs until present +// will be streamed at the beginning of the attach. +// The streamAttach parameter indicates that the attach itself will be streamed +// over the socket; if this is not set, but streamLogs is, only the logs will be +// sent. +// At least one of streamAttach and streamLogs must be set. +func (c *Container) HTTPAttach(httpCon net.Conn, httpBuf *bufio.ReadWriter, streams *HTTPAttachStreams, detachKeys *string, cancel <-chan bool, streamAttach, streamLogs bool) (deferredErr error) { + isTerminal := false + if c.config.Spec.Process != nil { + isTerminal = c.config.Spec.Process.Terminal + } + // Ensure our contract of writing errors to and closing the HTTP conn is + // honored. + defer func() { + hijackWriteErrorAndClose(deferredErr, c.ID(), isTerminal, httpCon, httpBuf) + }() + if !c.batched { c.lock.Lock() if err := c.syncContainer(); err != nil { c.lock.Unlock() - // Write any errors to the HTTP buffer before we close. - hijackWriteErrorAndClose(err, c.ID(), httpCon, httpBuf) - return err } // We are NOT holding the lock for the duration of the function. @@ -260,16 +276,80 @@ func (c *Container) HTTPAttach(httpCon net.Conn, httpBuf *bufio.ReadWriter, stre } if !c.ensureState(define.ContainerStateCreated, define.ContainerStateRunning) { - toReturn := errors.Wrapf(define.ErrCtrStateInvalid, "can only attach to created or running containers") - - // Write any errors to the HTTP buffer before we close. - hijackWriteErrorAndClose(toReturn, c.ID(), httpCon, httpBuf) + return errors.Wrapf(define.ErrCtrStateInvalid, "can only attach to created or running containers") + } - return toReturn + if !streamAttach && !streamLogs { + return errors.Wrapf(define.ErrInvalidArg, "must specify at least one of stream or logs") } logrus.Infof("Performing HTTP Hijack attach to container %s", c.ID()) + if streamLogs { + // Get all logs for the container + logChan := make(chan *logs.LogLine) + logOpts := new(logs.LogOptions) + logOpts.Tail = -1 + logOpts.WaitGroup = new(sync.WaitGroup) + errChan := make(chan error) + go func() { + var err error + // In non-terminal mode we need to prepend with the + // stream header. + logrus.Debugf("Writing logs for container %s to HTTP attach", c.ID()) + for logLine := range logChan { + if !isTerminal { + device := logLine.Device + var header []byte + headerLen := uint32(len(logLine.Msg)) + + switch strings.ToLower(device) { + case "stdin": + header = makeHTTPAttachHeader(0, headerLen) + case "stdout": + header = makeHTTPAttachHeader(1, headerLen) + case "stderr": + header = makeHTTPAttachHeader(2, headerLen) + default: + logrus.Errorf("Unknown device for log line: %s", device) + header = makeHTTPAttachHeader(1, headerLen) + } + _, err = httpBuf.Write(header) + if err != nil { + break + } + } + _, err = httpBuf.Write([]byte(logLine.Msg)) + if err != nil { + break + } + _, err = httpBuf.Write([]byte("\n")) + if err != nil { + break + } + err = httpBuf.Flush() + if err != nil { + break + } + } + errChan <- err + }() + go func() { + logOpts.WaitGroup.Wait() + close(logChan) + }() + if err := c.ReadLog(logOpts, logChan); err != nil { + return err + } + logrus.Debugf("Done reading logs for container %s", c.ID()) + if err := <-errChan; err != nil { + return err + } + } + if !streamAttach { + return nil + } + c.newContainerEvent(events.Attach) return c.ociRuntime.HTTPAttach(c, httpCon, httpBuf, streams, detachKeys, cancel) } diff --git a/libpod/oci_conmon_linux.go b/libpod/oci_conmon_linux.go index c20e3f0b4..18b438792 100644 --- a/libpod/oci_conmon_linux.go +++ b/libpod/oci_conmon_linux.go @@ -5,7 +5,6 @@ package libpod import ( "bufio" "bytes" - "encoding/binary" "fmt" "io" "io/ioutil" @@ -480,8 +479,7 @@ func (r *ConmonOCIRuntime) UnpauseContainer(ctr *Container) error { } // HTTPAttach performs an attach for the HTTP API. -// This will consume, and automatically close, the hijacked HTTP session. -// It is not necessary to close it independently. +// The caller must handle closing the HTTP connection after this returns. // The cancel channel is not closed; it is up to the caller to do so after // this function returns. // If this is a container with a terminal, we will stream raw. If it is not, we @@ -492,13 +490,7 @@ func (r *ConmonOCIRuntime) HTTPAttach(ctr *Container, httpConn net.Conn, httpBuf isTerminal = ctr.config.Spec.Process.Terminal } - // Ensure that our contract of closing the HTTP connection is honored. - defer hijackWriteErrorAndClose(deferredErr, ctr.ID(), httpConn, httpBuf) - if streams != nil { - if isTerminal { - return errors.Wrapf(define.ErrInvalidArg, "cannot specify which streams to attach as container %s has a terminal", ctr.ID()) - } if !streams.Stdin && !streams.Stdout && !streams.Stderr { return errors.Wrapf(define.ErrInvalidArg, "must specify at least one stream to attach to") } @@ -547,8 +539,16 @@ func (r *ConmonOCIRuntime) HTTPAttach(ctr *Container, httpConn net.Conn, httpBuf go func() { var err error if isTerminal { + // Hack: return immediately if attachStdout not set to + // emulate Docker. + // Basically, when terminal is set, STDERR goes nowhere. + // Everything does over STDOUT. + // Therefore, if not attaching STDOUT - we'll never copy + // anything from here. logrus.Debugf("Performing terminal HTTP attach for container %s", ctr.ID()) - err = httpAttachTerminalCopy(conn, httpBuf, ctr.ID()) + if attachStdout { + err = httpAttachTerminalCopy(conn, httpBuf, ctr.ID()) + } } else { logrus.Debugf("Performing non-terminal HTTP attach for container %s", ctr.ID()) err = httpAttachNonTerminalCopy(conn, httpBuf, ctr.ID(), attachStdin, attachStdout, attachStderr) @@ -1725,13 +1725,16 @@ func httpAttachNonTerminalCopy(container *net.UnixConn, http *bufio.ReadWriter, for { numR, err := container.Read(buf) if numR > 0 { - headerBuf := []byte{0, 0, 0, 0} + var headerBuf []byte + // Subtract 1 because we strip the first byte (used for + // multiplexing by Conmon). + headerLen := uint32(numR - 1) // Practically speaking, we could make this buf[0] - 1, // but we need to validate it anyways... switch buf[0] { case AttachPipeStdin: - headerBuf[0] = 0 + headerBuf = makeHTTPAttachHeader(0, headerLen) if !stdin { continue } @@ -1739,24 +1742,17 @@ func httpAttachNonTerminalCopy(container *net.UnixConn, http *bufio.ReadWriter, if !stdout { continue } - headerBuf[0] = 1 + headerBuf = makeHTTPAttachHeader(1, headerLen) case AttachPipeStderr: if !stderr { continue } - headerBuf[0] = 2 + headerBuf = makeHTTPAttachHeader(2, headerLen) default: logrus.Errorf("Received unexpected attach type %+d, discarding %d bytes", buf[0], numR) continue } - // Get big-endian length and append. - // Subtract 1 because we strip the first byte (used for - // multiplexing by Conmon). - lenBuf := []byte{0, 0, 0, 0} - binary.BigEndian.PutUint32(lenBuf, uint32(numR-1)) - headerBuf = append(headerBuf, lenBuf...) - numH, err2 := http.Write(headerBuf) if err2 != nil { if err != nil { diff --git a/libpod/util.go b/libpod/util.go index e9d234bbe..6457dac1c 100644 --- a/libpod/util.go +++ b/libpod/util.go @@ -2,6 +2,7 @@ package libpod import ( "bufio" + "encoding/binary" "fmt" "io" "os" @@ -239,11 +240,22 @@ func checkDependencyContainer(depCtr, ctr *Container) error { // hijackWriteErrorAndClose writes an error to a hijacked HTTP session and // closes it. Intended to HTTPAttach function. // If error is nil, it will not be written; we'll only close the connection. -func hijackWriteErrorAndClose(toWrite error, cid string, httpCon io.Closer, httpBuf *bufio.ReadWriter) { +func hijackWriteErrorAndClose(toWrite error, cid string, terminal bool, httpCon io.Closer, httpBuf *bufio.ReadWriter) { if toWrite != nil { - if _, err := httpBuf.Write([]byte(toWrite.Error())); err != nil { - logrus.Errorf("Error writing error %q to container %s HTTP attach connection: %v", toWrite, cid, err) - } else if err := httpBuf.Flush(); err != nil { + errString := []byte(fmt.Sprintf("%v\n", toWrite)) + if !terminal { + // We need a header. + header := makeHTTPAttachHeader(2, uint32(len(errString))) + if _, err := httpBuf.Write(header); err != nil { + logrus.Errorf("Error writing header for container %s attach connection error: %v", cid, err) + } + // TODO: May want to return immediately here to avoid + // writing garbage to the socket? + } + if _, err := httpBuf.Write(errString); err != nil { + logrus.Errorf("Error writing error to container %s HTTP attach connection: %v", cid, err) + } + if err := httpBuf.Flush(); err != nil { logrus.Errorf("Error flushing HTTP buffer for container %s HTTP attach connection: %v", cid, err) } } @@ -252,3 +264,14 @@ func hijackWriteErrorAndClose(toWrite error, cid string, httpCon io.Closer, http logrus.Errorf("Error closing container %s HTTP attach connection: %v", cid, err) } } + +// makeHTTPAttachHeader makes an 8-byte HTTP header for a buffer of the given +// length and stream. Accepts an integer indicating which stream we are sending +// to (STDIN = 0, STDOUT = 1, STDERR = 2). +func makeHTTPAttachHeader(stream byte, length uint32) []byte { + headerBuf := []byte{stream, 0, 0, 0} + lenBuf := []byte{0, 0, 0, 0} + binary.BigEndian.PutUint32(lenBuf, length) + headerBuf = append(headerBuf, lenBuf...) + return headerBuf +} diff --git a/pkg/api/handlers/compat/containers_attach.go b/pkg/api/handlers/compat/containers_attach.go index da7b5bb0c..80ad52aee 100644 --- a/pkg/api/handlers/compat/containers_attach.go +++ b/pkg/api/handlers/compat/containers_attach.go @@ -1,6 +1,7 @@ package compat import ( + "fmt" "net/http" "github.com/containers/libpod/libpod" @@ -23,7 +24,9 @@ func AttachContainer(w http.ResponseWriter, r *http.Request) { Stdin bool `schema:"stdin"` Stdout bool `schema:"stdout"` Stderr bool `schema:"stderr"` - }{} + }{ + Stream: true, + } if err := decoder.Decode(&query, r.URL.Query()); err != nil { utils.Error(w, "Error parsing parameters", http.StatusBadRequest, err) return @@ -61,16 +64,9 @@ func AttachContainer(w http.ResponseWriter, r *http.Request) { return } - // TODO: Investigate supporting these. - // Logs replays container logs over the attach socket. - // Stream seems to break things up somehow? Not 100% clear. - if query.Logs { - utils.Error(w, "Unsupported parameter", http.StatusBadRequest, errors.Errorf("the logs parameter to attach is not presently supported")) - return - } - // We only support stream=true or unset - if _, found := r.URL.Query()["stream"]; found && query.Stream { - utils.Error(w, "Unsupported parameter", http.StatusBadRequest, errors.Errorf("the stream parameter to attach is not presently supported")) + // At least one of these must be set + if !query.Stream && !query.Logs { + utils.Error(w, "Unsupported parameter", http.StatusBadRequest, errors.Errorf("at least one of Logs or Stream must be set")) return } @@ -86,7 +82,13 @@ func AttachContainer(w http.ResponseWriter, r *http.Request) { utils.InternalServerError(w, err) return } - if !(state == define.ContainerStateCreated || state == define.ContainerStateRunning) { + // For Docker compatibility, we need to re-initialize containers in these states. + if state == define.ContainerStateConfigured || state == define.ContainerStateExited { + if err := ctr.Init(r.Context()); err != nil { + utils.InternalServerError(w, errors.Wrapf(err, "error preparing container %s for attach", ctr.ID())) + return + } + } else if !(state == define.ContainerStateCreated || state == define.ContainerStateRunning) { utils.InternalServerError(w, errors.Wrapf(define.ErrCtrStateInvalid, "can only attach to created or running containers")) return } @@ -98,20 +100,23 @@ func AttachContainer(w http.ResponseWriter, r *http.Request) { return } - w.WriteHeader(http.StatusSwitchingProtocols) - connection, buffer, err := hijacker.Hijack() if err != nil { utils.InternalServerError(w, errors.Wrapf(err, "error hijacking connection")) return } + // This header string sourced from Docker: + // https://raw.githubusercontent.com/moby/moby/b95fad8e51bd064be4f4e58a996924f343846c85/api/server/router/container/container_routes.go + // Using literally to ensure compatability with existing clients. + fmt.Fprintf(connection, "HTTP/1.1 101 UPGRADED\r\nContent-Type: application/vnd.docker.raw-stream\r\nConnection: Upgrade\r\nUpgrade: tcp\r\n\r\n") + logrus.Debugf("Hijack for attach of container %s successful", ctr.ID()) // Perform HTTP attach. // HTTPAttach will handle everything about the connection from here on // (including closing it and writing errors to it). - if err := ctr.HTTPAttach(connection, buffer, streams, detachKeys, nil); err != nil { + if err := ctr.HTTPAttach(connection, buffer, streams, detachKeys, nil, query.Stream, query.Logs); err != nil { // We can't really do anything about errors anymore. HTTPAttach // should be writing them to the connection. logrus.Errorf("Error attaching to container %s: %v", ctr.ID(), err) diff --git a/pkg/api/handlers/compat/events.go b/pkg/api/handlers/compat/events.go index 0f72ef328..8ef32716d 100644 --- a/pkg/api/handlers/compat/events.go +++ b/pkg/api/handlers/compat/events.go @@ -1,7 +1,6 @@ package compat import ( - "encoding/json" "fmt" "net/http" @@ -10,6 +9,7 @@ import ( "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/gorilla/schema" + jsoniter "github.com/json-iterator/go" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -48,14 +48,27 @@ func GetEvents(w http.ResponseWriter, r *http.Request) { }() if eventsError != nil { utils.InternalServerError(w, eventsError) + close(eventChannel) return } - coder := json.NewEncoder(w) - coder.SetEscapeHTML(true) + // If client disappears we need to stop listening for events + go func(done <-chan struct{}) { + <-done + close(eventChannel) + }(r.Context().Done()) + // Headers need to be written out before turning Writer() over to json encoder w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) + if flusher, ok := w.(http.Flusher); ok { + flusher.Flush() + } + + json := jsoniter.ConfigCompatibleWithStandardLibrary + coder := json.NewEncoder(w) + coder.SetEscapeHTML(true) + for event := range eventChannel { e := handlers.EventToApiEvent(event) if err := coder.Encode(e); err != nil { diff --git a/pkg/api/handlers/compat/images_search.go b/pkg/api/handlers/compat/images_search.go index 7283b22c4..8da685527 100644 --- a/pkg/api/handlers/compat/images_search.go +++ b/pkg/api/handlers/compat/images_search.go @@ -57,6 +57,7 @@ func SearchImages(w http.ResponseWriter, r *http.Request) { Filter: filter, Limit: query.Limit, } + results, err := image.SearchImages(query.Term, options) if err != nil { utils.BadRequest(w, "term", query.Term, err) diff --git a/pkg/api/handlers/libpod/containers.go b/pkg/api/handlers/libpod/containers.go index 5cbfb11eb..086bef847 100644 --- a/pkg/api/handlers/libpod/containers.go +++ b/pkg/api/handlers/libpod/containers.go @@ -285,3 +285,23 @@ func Restore(w http.ResponseWriter, r *http.Request) { } utils.WriteResponse(w, http.StatusOK, entities.RestoreReport{Id: ctr.ID()}) } + +func InitContainer(w http.ResponseWriter, r *http.Request) { + name := utils.GetName(r) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + ctr, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + err = ctr.Init(r.Context()) + if errors.Cause(err) == define.ErrCtrStateInvalid { + utils.Error(w, "container already initialized", http.StatusNotModified, err) + return + } + if err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusNoContent, "") +} diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index 850de4598..a42d06205 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -645,3 +645,56 @@ func UntagImage(w http.ResponseWriter, r *http.Request) { } utils.WriteResponse(w, http.StatusCreated, "") } + +func SearchImages(w http.ResponseWriter, r *http.Request) { + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Term string `json:"term"` + Limit int `json:"limit"` + Filters []string `json:"filters"` + TLSVerify bool `json:"tlsVerify"` + }{ + // This is where you can override the golang default value for one of fields + } + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + + options := image.SearchOptions{ + Limit: query.Limit, + } + if _, found := r.URL.Query()["tlsVerify"]; found { + options.InsecureSkipTLSVerify = types.NewOptionalBool(!query.TLSVerify) + } + + if _, found := r.URL.Query()["filters"]; found { + filter, err := image.ParseSearchFilter(query.Filters) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse filters parameter for %s", r.URL.String())) + return + } + options.Filter = *filter + } + + searchResults, err := image.SearchImages(query.Term, options) + if err != nil { + utils.BadRequest(w, "term", query.Term, err) + return + } + // Convert from image.SearchResults to entities.ImageSearchReport. We don't + // want to leak any low-level packages into the remote client, which + // requires converting. + reports := make([]entities.ImageSearchReport, len(searchResults)) + for i := range searchResults { + reports[i].Index = searchResults[i].Index + reports[i].Name = searchResults[i].Name + reports[i].Description = searchResults[i].Index + reports[i].Stars = searchResults[i].Stars + reports[i].Official = searchResults[i].Official + reports[i].Automated = searchResults[i].Automated + } + + utils.WriteResponse(w, http.StatusOK, reports) +} diff --git a/pkg/api/handlers/libpod/pods.go b/pkg/api/handlers/libpod/pods.go index a890169a1..81cab1ede 100644 --- a/pkg/api/handlers/libpod/pods.go +++ b/pkg/api/handlers/libpod/pods.go @@ -12,6 +12,7 @@ import ( "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/specgen" + "github.com/containers/libpod/pkg/specgen/generate" "github.com/containers/libpod/pkg/util" "github.com/gorilla/schema" "github.com/pkg/errors" @@ -27,7 +28,7 @@ func PodCreate(w http.ResponseWriter, r *http.Request) { utils.Error(w, "Failed to decode specgen", http.StatusInternalServerError, errors.Wrap(err, "failed to decode specgen")) return } - pod, err := psg.MakePod(runtime) + pod, err := generate.MakePod(&psg, runtime) if err != nil { http_code := http.StatusInternalServerError if errors.Cause(err) == define.ErrPodExists { diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go index f1c932ebc..0fe6ae6a7 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -180,6 +180,31 @@ type ExecCreateResponse struct { docker.IDResponse } +func (e *Event) ToLibpodEvent() *events.Event { + exitCode, err := strconv.Atoi(e.Actor.Attributes["containerExitCode"]) + if err != nil { + return nil + } + status, err := events.StringToStatus(e.Action) + if err != nil { + return nil + } + t, err := events.StringToType(e.Type) + if err != nil { + return nil + } + lp := events.Event{ + ContainerExitCode: exitCode, + ID: e.Actor.ID, + Image: e.Actor.Attributes["image"], + Name: e.Actor.Attributes["name"], + Status: status, + Time: time.Unix(e.Time, e.TimeNano), + Type: t, + } + return &lp +} + func EventToApiEvent(e *events.Event) *Event { return &Event{dockerEvents.Message{ Type: e.Type.String(), diff --git a/pkg/api/server/handler_api.go b/pkg/api/server/handler_api.go index 30a1680c9..7a7db12f3 100644 --- a/pkg/api/server/handler_api.go +++ b/pkg/api/server/handler_api.go @@ -19,7 +19,7 @@ func (s *APIServer) APIHandler(h http.HandlerFunc) http.HandlerFunc { if err != nil { buf := make([]byte, 1<<20) n := runtime.Stack(buf, true) - log.Warnf("Recovering from podman handler panic: %v, %s", err, buf[:n]) + log.Warnf("Recovering from API handler panic: %v, %s", err, buf[:n]) // Try to inform client things went south... won't work if handler already started writing response body utils.InternalServerError(w, fmt.Errorf("%v", err)) } @@ -27,12 +27,7 @@ func (s *APIServer) APIHandler(h http.HandlerFunc) http.HandlerFunc { // Wrapper to hide some boiler plate fn := func(w http.ResponseWriter, r *http.Request) { - // Connection counting, ugh. Needed to support the sliding window for idle checking. - s.ConnectionCh <- EnterHandler - defer func() { s.ConnectionCh <- ExitHandler }() - - log.Debugf("APIHandler -- Method: %s URL: %s (conn %d/%d)", - r.Method, r.URL.String(), s.ActiveConnections, s.TotalConnections) + log.Debugf("APIHandler -- Method: %s URL: %s", r.Method, r.URL.String()) if err := r.ParseForm(); err != nil { log.Infof("Failed Request: unable to parse form: %q", err) diff --git a/pkg/api/server/register_containers.go b/pkg/api/server/register_containers.go index 150cca872..8b9a9e312 100644 --- a/pkg/api/server/register_containers.go +++ b/pkg/api/server/register_containers.go @@ -517,13 +517,13 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // name: logs // required: false // type: boolean - // description: Not yet supported + // description: Stream all logs from the container across the connection. Happens before streaming attach (if requested). At least one of logs or stream must be set // - in: query // name: stream // required: false // type: boolean // default: true - // description: If passed, must be set to true; stream=false is not yet supported + // description: Attach to the container. If unset, and logs is set, only the container's logs will be sent. At least one of stream or logs must be set // - in: query // name: stdout // required: false @@ -1194,13 +1194,13 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // name: logs // required: false // type: boolean - // description: Not yet supported + // description: Stream all logs from the container across the connection. Happens before streaming attach (if requested). At least one of logs or stream must be set // - in: query // name: stream // required: false // type: boolean // default: true - // description: If passed, must be set to true; stream=false is not yet supported + // description: Attach to the container. If unset, and logs is set, only the container's logs will be sent. At least one of stream or logs must be set // - in: query // name: stdout // required: false @@ -1377,7 +1377,6 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // 500: // $ref: "#/responses/InternalError" r.HandleFunc(VersionedPath("/libpod/containers/{name}/restore"), s.APIHandler(libpod.Restore)).Methods(http.MethodPost) - // swagger:operation GET /containers/{name}/changes libpod libpodChangesContainer // swagger:operation GET /libpod/containers/{name}/changes compat changesContainer // --- @@ -1411,6 +1410,29 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { r.HandleFunc(VersionedPath("/containers/{name}/changes"), s.APIHandler(compat.Changes)) r.HandleFunc("/containers/{name}/changes", s.APIHandler(compat.Changes)) r.HandleFunc(VersionedPath("/libpod/containers/{name}/changes"), s.APIHandler(compat.Changes)) - + // swagger:operation POST /libpod/containers/{name}/init libpod libpodInitContainer + // --- + // tags: + // - containers + // summary: Initialize a container + // description: Performs all tasks necessary for initializing the container but does not start the container. + // parameters: + // - in: path + // name: name + // type: string + // required: true + // description: the name or ID of the container + // produces: + // - application/json + // responses: + // 204: + // description: no error + // 304: + // description: container already initialized + // 404: + // $ref: "#/responses/NoSuchContainer" + // 500: + // $ref: "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/{name}/init"), s.APIHandler(libpod.InitContainer)).Methods(http.MethodPost) return nil } diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go index 77560e789..7dd887037 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -919,7 +919,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // $ref: "#/responses/DocsSearchResponse" // 500: // $ref: '#/responses/InternalError' - r.Handle(VersionedPath("/libpod/images/search"), s.APIHandler(compat.SearchImages)).Methods(http.MethodGet) + r.Handle(VersionedPath("/libpod/images/search"), s.APIHandler(libpod.SearchImages)).Methods(http.MethodGet) // swagger:operation DELETE /libpod/images/{name:.*} libpod libpodRemoveImage // --- // tags: diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go index c1ec6aca8..5f1a86183 100644 --- a/pkg/api/server/server.go +++ b/pkg/api/server/server.go @@ -2,11 +2,14 @@ package server import ( "context" + "log" "net" "net/http" "os" "os/signal" + "runtime" "strings" + "sync" "syscall" "time" @@ -20,26 +23,19 @@ import ( ) type APIServer struct { - http.Server // The HTTP work happens here - *schema.Decoder // Decoder for Query parameters to structs - context.Context // Context to carry objects to handlers - *libpod.Runtime // Where the real work happens - net.Listener // mux for routing HTTP API calls to libpod routines - context.CancelFunc // Stop APIServer - *time.Timer // Hold timer for sliding window - time.Duration // Duration of client access sliding window - ActiveConnections uint64 // Number of handlers holding a connection - TotalConnections uint64 // Number of connections handled - ConnectionCh chan int // Channel for signalling handler enter/exit + http.Server // The HTTP work happens here + *schema.Decoder // Decoder for Query parameters to structs + context.Context // Context to carry objects to handlers + *libpod.Runtime // Where the real work happens + net.Listener // mux for routing HTTP API calls to libpod routines + context.CancelFunc // Stop APIServer + idleTracker *IdleTracker // Track connections to support idle shutdown } // Number of seconds to wait for next request, if exceeded shutdown server const ( DefaultServiceDuration = 300 * time.Second UnlimitedServiceDuration = 0 * time.Second - EnterHandler = 1 - ExitHandler = -1 - NOOPHandler = 0 ) // NewServer will create and configure a new API server with all defaults @@ -70,17 +66,20 @@ func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Li } router := mux.NewRouter().UseEncodedPath() + idle := NewIdleTracker(duration) + server := APIServer{ Server: http.Server{ Handler: router, ReadHeaderTimeout: 20 * time.Second, IdleTimeout: duration, + ConnState: idle.ConnState, + ErrorLog: log.New(logrus.StandardLogger().Out, "", 0), }, - Decoder: handlers.NewAPIDecoder(), - Runtime: runtime, - Listener: *listener, - Duration: duration, - ConnectionCh: make(chan int), + Decoder: handlers.NewAPIDecoder(), + idleTracker: idle, + Listener: *listener, + Runtime: runtime, } router.NotFoundHandler = http.HandlerFunc( @@ -120,11 +119,11 @@ func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Li router.Walk(func(route *mux.Route, r *mux.Router, ancestors []*mux.Route) error { // nolint path, err := route.GetPathTemplate() if err != nil { - path = "" + path = "<N/A>" } methods, err := route.GetMethods() if err != nil { - methods = []string{} + methods = []string{"<N/A>"} } logrus.Debugf("Methods: %s Path: %s", strings.Join(methods, ", "), path) return nil @@ -136,24 +135,20 @@ func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Li // Serve starts responding to HTTP requests func (s *APIServer) Serve() error { - // This is initialized here as Timer is not needed until Serve'ing - if s.Duration > 0 { - s.Timer = time.AfterFunc(s.Duration, func() { - s.ConnectionCh <- NOOPHandler - }) - go s.ReadChannelWithTimeout() - } else { - go s.ReadChannelNoTimeout() - } - sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) errChan := make(chan error, 1) go func() { + <-s.idleTracker.Done() + logrus.Debugf("API Server idle for %v", s.idleTracker.Duration) + _ = s.Shutdown() + }() + + go func() { err := s.Server.Serve(s.Listener) if err != nil && err != http.ErrServerClosed { - errChan <- errors.Wrap(err, "Failed to start APIServer") + errChan <- errors.Wrap(err, "failed to start API server") return } errChan <- nil @@ -169,72 +164,30 @@ func (s *APIServer) Serve() error { return nil } -func (s *APIServer) ReadChannelWithTimeout() { - // stalker to count the connections. Should the timer expire it will shutdown the service. - for delta := range s.ConnectionCh { - switch delta { - case EnterHandler: - s.Timer.Stop() - s.ActiveConnections += 1 - s.TotalConnections += 1 - case ExitHandler: - s.Timer.Stop() - s.ActiveConnections -= 1 - if s.ActiveConnections == 0 { - // Server will be shutdown iff the timer expires before being reset or stopped - s.Timer = time.AfterFunc(s.Duration, func() { - if err := s.Shutdown(); err != nil { - logrus.Errorf("Failed to shutdown APIServer: %v", err) - os.Exit(1) - } - }) - } else { - s.Timer.Reset(s.Duration) - } - case NOOPHandler: - // push the check out another duration... - s.Timer.Reset(s.Duration) - default: - logrus.Warnf("ConnectionCh received unsupported input %d", delta) - } - } -} - -func (s *APIServer) ReadChannelNoTimeout() { - // stalker to count the connections. - for delta := range s.ConnectionCh { - switch delta { - case EnterHandler: - s.ActiveConnections += 1 - s.TotalConnections += 1 - case ExitHandler: - s.ActiveConnections -= 1 - case NOOPHandler: - default: - logrus.Warnf("ConnectionCh received unsupported input %d", delta) - } - } -} - // Shutdown is a clean shutdown waiting on existing clients func (s *APIServer) Shutdown() error { + if logrus.IsLevelEnabled(logrus.DebugLevel) { + _, file, line, _ := runtime.Caller(1) + logrus.Debugf("APIServer.Shutdown by %s:%d, %d/%d connection(s)", + file, line, s.idleTracker.ActiveConnections(), s.idleTracker.TotalConnections()) + } + // Duration == 0 flags no auto-shutdown of the server - if s.Duration == 0 { + if s.idleTracker.Duration == 0 { logrus.Debug("APIServer.Shutdown ignored as Duration == 0") return nil } - logrus.Debugf("APIServer.Shutdown called %v, conn %d/%d", time.Now(), s.ActiveConnections, s.TotalConnections) - // Gracefully shutdown server - ctx, cancel := context.WithTimeout(context.Background(), s.Duration) + // Gracefully shutdown server, duration of wait same as idle window + ctx, cancel := context.WithTimeout(context.Background(), s.idleTracker.Duration) defer cancel() - go func() { err := s.Server.Shutdown(ctx) if err != nil && err != context.Canceled && err != http.ErrServerClosed { logrus.Errorf("Failed to cleanly shutdown APIServer: %s", err.Error()) } }() + <-ctx.Done() return nil } @@ -242,3 +195,55 @@ func (s *APIServer) Shutdown() error { func (s *APIServer) Close() error { return s.Server.Close() } + +type IdleTracker struct { + active map[net.Conn]struct{} + total int + mux sync.Mutex + timer *time.Timer + Duration time.Duration +} + +func NewIdleTracker(idle time.Duration) *IdleTracker { + return &IdleTracker{ + active: make(map[net.Conn]struct{}), + Duration: idle, + timer: time.NewTimer(idle), + } +} + +func (t *IdleTracker) ConnState(conn net.Conn, state http.ConnState) { + t.mux.Lock() + defer t.mux.Unlock() + + oldActive := len(t.active) + logrus.Debugf("IdleTracker %p:%v %d/%d connection(s)", conn, state, t.ActiveConnections(), t.TotalConnections()) + switch state { + case http.StateNew, http.StateActive, http.StateHijacked: + t.active[conn] = struct{}{} + // stop the timer if we transitioned from idle + if oldActive == 0 { + t.timer.Stop() + } + t.total += 1 + case http.StateIdle, http.StateClosed: + delete(t.active, conn) + // Restart the timer if we've become idle + if oldActive > 0 && len(t.active) == 0 { + t.timer.Stop() + t.timer.Reset(t.Duration) + } + } +} + +func (t *IdleTracker) ActiveConnections() int { + return len(t.active) +} + +func (t *IdleTracker) TotalConnections() int { + return t.total +} + +func (t *IdleTracker) Done() <-chan time.Time { + return t.timer.C +} diff --git a/pkg/bindings/containers/containers.go b/pkg/bindings/containers/containers.go index a188d73a0..963f0ec57 100644 --- a/pkg/bindings/containers/containers.go +++ b/pkg/bindings/containers/containers.go @@ -12,6 +12,7 @@ import ( "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/bindings" "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" ) // List obtains a list of containers in local storage. All parameters to this method are optional. @@ -316,3 +317,21 @@ func Export(ctx context.Context, nameOrID string, w io.Writer) error { } return response.Process(nil) } + +// ContainerInit takes a created container and executes all of the +// preparations to run the container except it will not start +// or attach to the container +func ContainerInit(ctx context.Context, nameOrID string) error { + conn, err := bindings.GetClient(ctx) + if err != nil { + return err + } + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/init", nil, nameOrID) + if err != nil { + return err + } + if response.StatusCode == http.StatusNotModified { + return errors.Wrapf(define.ErrCtrStateInvalid, "container %s has already been created in runtime", nameOrID) + } + return response.Process(nil) +} diff --git a/pkg/bindings/images/images.go b/pkg/bindings/images/images.go index 1b3df609b..3550c3968 100644 --- a/pkg/bindings/images/images.go +++ b/pkg/bindings/images/images.go @@ -2,7 +2,6 @@ package images import ( "context" - "errors" "fmt" "io" "net/http" @@ -13,6 +12,7 @@ import ( "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/bindings" "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" ) // Exists a lightweight way to determine if an image exists in local storage. It returns a @@ -308,3 +308,34 @@ func Push(ctx context.Context, source string, destination string, options entiti _, err = conn.DoRequest(nil, http.MethodPost, path, params) return err } + +// Search is the binding for libpod's v2 endpoints for Search images. +func Search(ctx context.Context, term string, opts entities.ImageSearchOptions) ([]entities.ImageSearchReport, error) { + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + params := url.Values{} + params.Set("term", term) + params.Set("limit", strconv.Itoa(opts.Limit)) + for _, f := range opts.Filters { + params.Set("filters", f) + } + + if opts.TLSVerify != types.OptionalBoolUndefined { + val := bool(opts.TLSVerify == types.OptionalBoolTrue) + params.Set("tlsVerify", strconv.FormatBool(val)) + } + + response, err := conn.DoRequest(nil, http.MethodGet, "/images/search", params) + if err != nil { + return nil, err + } + + results := []entities.ImageSearchReport{} + if err := response.Process(&results); err != nil { + return nil, err + } + + return results, nil +} diff --git a/pkg/bindings/images/search.go b/pkg/bindings/images/search.go deleted file mode 100644 index 183ff3d77..000000000 --- a/pkg/bindings/images/search.go +++ /dev/null @@ -1,41 +0,0 @@ -package images - -import ( - "context" - "net/http" - "net/url" - "strconv" - - "github.com/containers/libpod/libpod/image" - "github.com/containers/libpod/pkg/bindings" -) - -// Search looks for the given image (term) in container image registries. The optional limit parameter sets -// a maximum number of results returned. The optional filters parameter allow for more specific image -// searches. -func Search(ctx context.Context, term string, limit *int, filters map[string][]string) ([]image.SearchResult, error) { - var ( - searchResults []image.SearchResult - ) - conn, err := bindings.GetClient(ctx) - if err != nil { - return nil, err - } - params := url.Values{} - params.Set("term", term) - if limit != nil { - params.Set("limit", strconv.Itoa(*limit)) - } - if filters != nil { - stringFilter, err := bindings.FiltersToString(filters) - if err != nil { - return nil, err - } - params.Set("filters", stringFilter) - } - response, err := conn.DoRequest(nil, http.MethodGet, "/images/search", params) - if err != nil { - return searchResults, nil - } - return searchResults, response.Process(&searchResults) -} diff --git a/pkg/bindings/test/containers_test.go b/pkg/bindings/test/containers_test.go index d3fc80eea..0b1b9ecdd 100644 --- a/pkg/bindings/test/containers_test.go +++ b/pkg/bindings/test/containers_test.go @@ -508,8 +508,27 @@ var _ = Describe("Podman containers ", func() { _, err = bt.RunTopContainer(&name2, &bindings.PFalse, nil) Expect(err).To(BeNil()) containerLatestList, err := containers.List(bt.conn, nil, nil, &latestContainers, nil, nil, nil) - err = containers.Kill(bt.conn, containerLatestList[0].Names(), "SIGTERM") Expect(err).To(BeNil()) + err = containers.Kill(bt.conn, containerLatestList[0].Names[0], "SIGTERM") + Expect(err).To(BeNil()) + }) + + It("container init on a bogus container", func() { + err := containers.ContainerInit(bt.conn, "doesnotexist") + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusNotFound)) + }) + + It("container init", func() { + s := specgen.NewSpecGenerator(alpine.name) + ctr, err := containers.CreateWithSpec(bt.conn, s) + Expect(err).To(BeNil()) + err = containers.ContainerInit(bt.conn, ctr.ID) + Expect(err).To(BeNil()) + // trying to init again should be an error + err = containers.ContainerInit(bt.conn, ctr.ID) + Expect(err).ToNot(BeNil()) }) }) diff --git a/pkg/bindings/test/images_test.go b/pkg/bindings/test/images_test.go index 992720196..58210efd0 100644 --- a/pkg/bindings/test/images_test.go +++ b/pkg/bindings/test/images_test.go @@ -314,11 +314,11 @@ var _ = Describe("Podman images", func() { }) It("Search for an image", func() { - imgs, err := images.Search(bt.conn, "alpine", nil, nil) + reports, err := images.Search(bt.conn, "alpine", entities.ImageSearchOptions{}) Expect(err).To(BeNil()) - Expect(len(imgs)).To(BeNumerically(">", 1)) + Expect(len(reports)).To(BeNumerically(">", 1)) var foundAlpine bool - for _, i := range imgs { + for _, i := range reports { if i.Name == "docker.io/library/alpine" { foundAlpine = true break @@ -327,23 +327,20 @@ var _ = Describe("Podman images", func() { Expect(foundAlpine).To(BeTrue()) // Search for alpine with a limit of 10 - ten := 10 - imgs, err = images.Search(bt.conn, "docker.io/alpine", &ten, nil) + reports, err = images.Search(bt.conn, "docker.io/alpine", entities.ImageSearchOptions{Limit: 10}) Expect(err).To(BeNil()) - Expect(len(imgs)).To(BeNumerically("<=", 10)) + Expect(len(reports)).To(BeNumerically("<=", 10)) // Search for alpine with stars greater than 100 - filters := make(map[string][]string) - filters["stars"] = []string{"100"} - imgs, err = images.Search(bt.conn, "docker.io/alpine", nil, filters) + reports, err = images.Search(bt.conn, "docker.io/alpine", entities.ImageSearchOptions{Filters: []string{"stars=100"}}) Expect(err).To(BeNil()) - for _, i := range imgs { + for _, i := range reports { Expect(i.Stars).To(BeNumerically(">=", 100)) } // Search with a fqdn - imgs, err = images.Search(bt.conn, "quay.io/libpod/alpine_nginx", nil, nil) - Expect(len(imgs)).To(BeNumerically(">=", 1)) + reports, err = images.Search(bt.conn, "quay.io/libpod/alpine_nginx", entities.ImageSearchOptions{}) + Expect(len(reports)).To(BeNumerically(">=", 1)) }) It("Prune images", func() { diff --git a/pkg/cgroups/pids.go b/pkg/cgroups/pids.go index 92f82553b..b2bfebe4d 100644 --- a/pkg/cgroups/pids.go +++ b/pkg/cgroups/pids.go @@ -44,7 +44,7 @@ func (c *pidHandler) Destroy(ctr *CgroupControl) error { // Stat fills a metrics structure with usage stats for the controller func (c *pidHandler) Stat(ctr *CgroupControl, m *Metrics) error { - if ctr.path != "" { + if ctr.path == "" { // nothing we can do to retrieve the pids.current path return nil } diff --git a/pkg/domain/entities/container_ps.go b/pkg/domain/entities/container_ps.go index f07b0364f..ceafecebc 100644 --- a/pkg/domain/entities/container_ps.go +++ b/pkg/domain/entities/container_ps.go @@ -1,23 +1,19 @@ package entities import ( - "fmt" "sort" - "strconv" "strings" - "time" "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" "github.com/cri-o/ocicni/pkg/ocicni" - "github.com/docker/go-units" "github.com/pkg/errors" ) // Listcontainer describes a container suitable for listing type ListContainer struct { // Container command - Cmd []string + Command []string // Container creation time Created int64 // If container has exited/stopped @@ -37,7 +33,7 @@ type ListContainer struct { // User volume mounts Mounts []string // The names assigned to the container - ContainerNames []string + Names []string // Namespaces the container belongs to. Requires the // namespace boolean to be true Namespaces ListContainerNamespaces @@ -50,69 +46,13 @@ type ListContainer struct { // boolean to be set PodName string // Port mappings - PortMappings []ocicni.PortMapping + Ports []ocicni.PortMapping // Size of the container rootfs. Requires the size boolean to be true - ContainerSize *shared.ContainerSize + Size *shared.ContainerSize // Time when container started StartedAt int64 // State of container - ContainerState string -} - -// State returns the container state in human duration -func (l ListContainer) State() string { - var state string - switch l.ContainerState { - case "running": - t := units.HumanDuration(time.Since(time.Unix(l.StartedAt, 0))) - state = "Up " + t + " ago" - case "configured": - state = "Created" - case "exited", "stopped": - t := units.HumanDuration(time.Since(time.Unix(l.ExitedAt, 0))) - state = fmt.Sprintf("Exited (%d) %s ago", l.ExitCode, t) - default: - state = l.ContainerState - } - return state -} - -// Command returns the container command in string format -func (l ListContainer) Command() string { - return strings.Join(l.Cmd, " ") -} - -// Size returns the rootfs and virtual sizes in human duration in -// and output form (string) suitable for ps -func (l ListContainer) Size() string { - virt := units.HumanSizeWithPrecision(float64(l.ContainerSize.RootFsSize), 3) - s := units.HumanSizeWithPrecision(float64(l.ContainerSize.RwSize), 3) - return fmt.Sprintf("%s (virtual %s)", s, virt) -} - -// Names returns the container name in string format -func (l ListContainer) Names() string { - return l.ContainerNames[0] -} - -// Ports converts from Portmappings to the string form -// required by ps -func (l ListContainer) Ports() string { - if len(l.PortMappings) < 1 { - return "" - } - return portsToString(l.PortMappings) -} - -// CreatedAt returns the container creation time in string format. podman -// and docker both return a timestamped value for createdat -func (l ListContainer) CreatedAt() string { - return time.Unix(l.Created, 0).String() -} - -// CreateHuman allows us to output the created time in human readable format -func (l ListContainer) CreatedHuman() string { - return units.HumanDuration(time.Since(time.Unix(l.Created, 0))) + " ago" + State string } // ListContainer Namespaces contains the identifiers of the container's Linux namespaces @@ -153,7 +93,7 @@ func (a SortListContainers) Swap(i, j int) { a[i], a[j] = a[j], a[i] } type psSortedCommand struct{ SortListContainers } func (a psSortedCommand) Less(i, j int) bool { - return strings.Join(a.SortListContainers[i].Cmd, " ") < strings.Join(a.SortListContainers[j].Cmd, " ") + return strings.Join(a.SortListContainers[i].Command, " ") < strings.Join(a.SortListContainers[j].Command, " ") } type psSortedId struct{ SortListContainers } @@ -171,7 +111,7 @@ func (a psSortedImage) Less(i, j int) bool { type psSortedNames struct{ SortListContainers } func (a psSortedNames) Less(i, j int) bool { - return a.SortListContainers[i].ContainerNames[0] < a.SortListContainers[j].ContainerNames[0] + return a.SortListContainers[i].Names[0] < a.SortListContainers[j].Names[0] } type psSortedPod struct{ SortListContainers } @@ -189,16 +129,16 @@ func (a psSortedRunningFor) Less(i, j int) bool { type psSortedStatus struct{ SortListContainers } func (a psSortedStatus) Less(i, j int) bool { - return a.SortListContainers[i].ContainerState < a.SortListContainers[j].ContainerState + return a.SortListContainers[i].State < a.SortListContainers[j].State } type psSortedSize struct{ SortListContainers } func (a psSortedSize) Less(i, j int) bool { - if a.SortListContainers[i].ContainerSize == nil || a.SortListContainers[j].ContainerSize == nil { + if a.SortListContainers[i].Size == nil || a.SortListContainers[j].Size == nil { return false } - return a.SortListContainers[i].ContainerSize.RootFsSize < a.SortListContainers[j].ContainerSize.RootFsSize + return a.SortListContainers[i].Size.RootFsSize < a.SortListContainers[j].Size.RootFsSize } type PsSortedCreateTime struct{ SortListContainers } @@ -232,94 +172,3 @@ func SortPsOutput(sortBy string, psOutput SortListContainers) (SortListContainer } return psOutput, nil } - -// portsToString converts the ports used to a string of the from "port1, port2" -// and also groups a continuous list of ports into a readable format. -func portsToString(ports []ocicni.PortMapping) string { - type portGroup struct { - first int32 - last int32 - } - var portDisplay []string - if len(ports) == 0 { - return "" - } - //Sort the ports, so grouping continuous ports become easy. - sort.Slice(ports, func(i, j int) bool { - return comparePorts(ports[i], ports[j]) - }) - - // portGroupMap is used for grouping continuous ports. - portGroupMap := make(map[string]*portGroup) - var groupKeyList []string - - for _, v := range ports { - - hostIP := v.HostIP - if hostIP == "" { - hostIP = "0.0.0.0" - } - // If hostPort and containerPort are not same, consider as individual port. - if v.ContainerPort != v.HostPort { - portDisplay = append(portDisplay, fmt.Sprintf("%s:%d->%d/%s", hostIP, v.HostPort, v.ContainerPort, v.Protocol)) - continue - } - - portMapKey := fmt.Sprintf("%s/%s", hostIP, v.Protocol) - - portgroup, ok := portGroupMap[portMapKey] - if !ok { - portGroupMap[portMapKey] = &portGroup{first: v.ContainerPort, last: v.ContainerPort} - // This list is required to traverse portGroupMap. - groupKeyList = append(groupKeyList, portMapKey) - continue - } - - if portgroup.last == (v.ContainerPort - 1) { - portgroup.last = v.ContainerPort - continue - } - } - // For each portMapKey, format group list and appned to output string. - for _, portKey := range groupKeyList { - group := portGroupMap[portKey] - portDisplay = append(portDisplay, formatGroup(portKey, group.first, group.last)) - } - return strings.Join(portDisplay, ", ") -} - -func comparePorts(i, j ocicni.PortMapping) bool { - if i.ContainerPort != j.ContainerPort { - return i.ContainerPort < j.ContainerPort - } - - if i.HostIP != j.HostIP { - return i.HostIP < j.HostIP - } - - if i.HostPort != j.HostPort { - return i.HostPort < j.HostPort - } - - return i.Protocol < j.Protocol -} - -// formatGroup returns the group as <IP:startPort:lastPort->startPort:lastPort/Proto> -// e.g 0.0.0.0:1000-1006->1000-1006/tcp. -func formatGroup(key string, start, last int32) string { - parts := strings.Split(key, "/") - groupType := parts[0] - var ip string - if len(parts) > 1 { - ip = parts[0] - groupType = parts[1] - } - group := strconv.Itoa(int(start)) - if start != last { - group = fmt.Sprintf("%s-%d", group, last) - } - if ip != "" { - group = fmt.Sprintf("%s:%s->%s", ip, group, group) - } - return fmt.Sprintf("%s/%s", group, groupType) -} diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go index 51e6cc751..f21af9ce4 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -283,3 +283,47 @@ type ContainerCleanupReport struct { RmErr error RmiErr error } + +// ContainerInitOptions describes input options +// for the container init cli +type ContainerInitOptions struct { + All bool + Latest bool +} + +// ContainerInitReport describes the results of a +// container init +type ContainerInitReport struct { + Err error + Id string +} + +//ContainerMountOptions describes the input values for mounting containers +// in the CLI +type ContainerMountOptions struct { + All bool + Format string + Latest bool + NoTruncate bool +} + +// ContainerUnmountOptions are the options from the cli for unmounting +type ContainerUnmountOptions struct { + All bool + Force bool + Latest bool +} + +// ContainerMountReport describes the response from container mount +type ContainerMountReport struct { + Err error + Id string + Name string + Path string +} + +// ContainerUnmountReport describes the response from umounting a container +type ContainerUnmountReport struct { + Err error + Id string +} diff --git a/pkg/domain/entities/engine.go b/pkg/domain/entities/engine.go index c14348529..3b971a1e8 100644 --- a/pkg/domain/entities/engine.go +++ b/pkg/domain/entities/engine.go @@ -1,13 +1,23 @@ package entities import ( - "os/user" - "path/filepath" + "context" + "fmt" + "io" + "os" + "github.com/containers/buildah/pkg/parse" "github.com/containers/common/pkg/config" + "github.com/containers/common/pkg/sysinfo" + "github.com/containers/libpod/pkg/apparmor" + "github.com/containers/libpod/pkg/cgroups" + "github.com/containers/libpod/pkg/rootless" + "github.com/opencontainers/selinux/go-selinux" + "github.com/opentracing/opentracing-go" "github.com/spf13/pflag" ) +// EngineMode is the connection type podman is using to access libpod type EngineMode string const ( @@ -15,78 +25,243 @@ const ( TunnelMode = EngineMode("tunnel") ) +// Convert EngineMode to String func (m EngineMode) String() string { return string(m) } -type EngineOptions struct { - Uri string - Identities []string - FlagSet *pflag.FlagSet - EngineMode EngineMode - - CGroupManager string - CniConfigDir string - ConmonPath string - DefaultMountsFile string - EventsBackend string - HooksDir []string - MaxWorks int - Namespace string - Root string - Runroot string - Runtime string - StorageDriver string - StorageOpts []string - Syslog bool - Trace bool - NetworkCmdPath string - - Config string - CpuProfile string - LogLevel string - TmpDir string - - RemoteUserName string - RemoteHost string - VarlinkAddress string - ConnectionName string - RemoteConfigFilePath string - Port int - IdentityFile string - IgnoreHosts bool -} - -func NewEngineOptions() (EngineOptions, error) { - u, _ := user.Current() - return EngineOptions{ - CGroupManager: config.SystemdCgroupsManager, - CniConfigDir: "", - Config: "", - ConmonPath: filepath.Join("usr", "bin", "conmon"), - ConnectionName: "", - CpuProfile: "", - DefaultMountsFile: "", - EventsBackend: "", - HooksDir: nil, - IdentityFile: "", - IgnoreHosts: false, - LogLevel: "", - MaxWorks: 0, - Namespace: "", - NetworkCmdPath: "", - Port: 0, - RemoteConfigFilePath: "", - RemoteHost: "", - RemoteUserName: "", - Root: "", - Runroot: filepath.Join("run", "user", u.Uid), - Runtime: "", - StorageDriver: "overlayfs", - StorageOpts: nil, - Syslog: false, - TmpDir: filepath.Join("run", "user", u.Uid, "libpod", "tmp"), - Trace: false, - VarlinkAddress: "", - }, nil +// PodmanConfig combines the defaults and settings from the file system with the +// flags given in os.Args. Some runtime state is also stored here. +type PodmanConfig struct { + *config.Config + *pflag.FlagSet + + CGroupUsage string // rootless code determines Usage message + ConmonPath string // --conmon flag will set Engine.ConmonPath + CpuProfile string // Hidden: Should CPU profile be taken + EngineMode EngineMode // ABI or Tunneling mode + Identities []string // ssh identities for connecting to server + MaxWorks int // maximum number of parallel threads + RuntimePath string // --runtime flag will set Engine.RuntimePath + SpanCloser io.Closer // Close() for tracing object + SpanCtx context.Context // context to use when tracing + Span opentracing.Span // tracing object + Syslog bool // write to StdOut and Syslog, not supported when tunneling + Trace bool // Hidden: Trace execution + Uri string // URI to API Service + + Runroot string + StorageDriver string + StorageOpts []string +} + +// DefaultSecurityOptions: getter for security options from configuration +func (c PodmanConfig) DefaultSecurityOptions() []string { + securityOpts := []string{} + if c.Containers.SeccompProfile != "" && c.Containers.SeccompProfile != parse.SeccompDefaultPath { + securityOpts = append(securityOpts, fmt.Sprintf("seccomp=%s", c.Containers.SeccompProfile)) + } + if apparmor.IsEnabled() && c.Containers.ApparmorProfile != "" { + securityOpts = append(securityOpts, fmt.Sprintf("apparmor=%s", c.Containers.ApparmorProfile)) + } + if selinux.GetEnabled() && !c.Containers.EnableLabeling { + securityOpts = append(securityOpts, fmt.Sprintf("label=%s", selinux.DisableSecOpt()[0])) + } + return securityOpts +} + +// DefaultSysctls +func (c PodmanConfig) DefaultSysctls() []string { + return c.Containers.DefaultSysctls +} + +func (c PodmanConfig) DefaultVolumes() []string { + return c.Containers.Volumes +} + +func (c PodmanConfig) DefaultDevices() []string { + return c.Containers.Devices +} + +func (c PodmanConfig) DefaultDNSServers() []string { + return c.Containers.DNSServers +} + +func (c PodmanConfig) DefaultDNSSearches() []string { + return c.Containers.DNSSearches +} + +func (c PodmanConfig) DefaultDNSOptions() []string { + return c.Containers.DNSOptions +} + +func (c PodmanConfig) DefaultEnv() []string { + return c.Containers.Env +} + +func (c PodmanConfig) DefaultInitPath() string { + return c.Containers.InitPath +} + +func (c PodmanConfig) DefaultIPCNS() string { + return c.Containers.IPCNS } + +func (c PodmanConfig) DefaultPidNS() string { + return c.Containers.PidNS +} + +func (c PodmanConfig) DefaultNetNS() string { + if c.Containers.NetNS == "private" && rootless.IsRootless() { + return "slirp4netns" + } + return c.Containers.NetNS +} + +func (c PodmanConfig) DefaultCgroupNS() string { + return c.Containers.CgroupNS +} + +func (c PodmanConfig) DefaultUTSNS() string { + return c.Containers.UTSNS +} + +func (c PodmanConfig) DefaultShmSize() string { + return c.Containers.ShmSize +} + +func (c PodmanConfig) DefaultUlimits() []string { + return c.Containers.DefaultUlimits +} + +func (c PodmanConfig) DefaultUserNS() string { + if v, found := os.LookupEnv("PODMAN_USERNS"); found { + return v + } + return c.Containers.UserNS +} + +func (c PodmanConfig) DefaultPidsLimit() int64 { + if rootless.IsRootless() { + cgroup2, _ := cgroups.IsCgroup2UnifiedMode() + if cgroup2 { + return c.Containers.PidsLimit + } + } + return sysinfo.GetDefaultPidsLimit() +} + +func (c PodmanConfig) DefaultPidsDescription() string { + return "Tune container pids limit (set 0 for unlimited)" +} + +func (c PodmanConfig) DefaultDetachKeys() string { + return c.Engine.DetachKeys +} + +// TODO: Remove in rootless support PR +// // EngineOptions holds the environment for running the engines +// type EngineOptions struct { +// // Introduced with V2 +// Uri string +// Identities []string +// FlagSet *pflag.FlagSet +// EngineMode EngineMode +// CGroupUsage string +// +// // Introduced with V1 +// CGroupManager string // config.EngineConfig +// CniConfigDir string // config.NetworkConfig.NetworkConfigDir +// ConmonPath string // config.EngineConfig +// DefaultMountsFile string // config.ContainersConfig +// EventsBackend string // config.EngineConfig.EventsLogger +// HooksDir []string // config.EngineConfig +// MaxWorks int +// Namespace string // config.EngineConfig +// Root string // +// Runroot string // config.EngineConfig.StorageConfigRunRootSet?? +// Runtime string // config.EngineConfig.OCIRuntime +// StorageDriver string // config.EngineConfig.StorageConfigGraphDriverNameSet?? +// StorageOpts []string +// Syslog bool +// Trace bool +// NetworkCmdPath string // config.EngineConfig +// +// Config string +// CpuProfile string +// LogLevel string +// TmpDir string // config.EngineConfig +// +// RemoteUserName string // deprecated +// RemoteHost string // deprecated +// VarlinkAddress string // deprecated +// ConnectionName string +// RemoteConfigFilePath string +// Port int // deprecated +// IdentityFile string // deprecated +// IgnoreHosts bool +// } +// +// func NewEngineOptions(opts EngineOptions) (EngineOptions, error) { +// ctnrCfg, err := config.Default() +// if err != nil { +// logrus.Error(err) +// os.Exit(1) +// } +// +// cgroupManager := ctnrCfg.Engine.CgroupManager +// cgroupUsage := `Cgroup manager to use ("cgroupfs"|"systemd")` +// cgroupv2, _ := cgroups.IsCgroup2UnifiedMode() +// cniPluginDir := ctnrCfg.Network.CNIPluginDirs[0] +// +// cfg, err := config.NewConfig("") +// if err != nil { +// logrus.Errorf("Error loading container config %v\n", err) +// os.Exit(1) +// } +// cfg.CheckCgroupsAndAdjustConfig() +// +// if rootless.IsRootless() { +// if !cgroupv2 { +// cgroupManager = "" +// cgroupUsage = "Cgroup manager is not supported in rootless mode" +// } +// cniPluginDir = "" +// } +// +// return EngineOptions{ +// CGroupManager: cgroupManager, +// CGroupUsage: cgroupUsage, +// CniConfigDir: cniPluginDir, +// Config: opts.Config, // TODO: deprecate +// ConmonPath: opts.ConmonPath, +// ConnectionName: opts.ConnectionName, +// CpuProfile: opts.CpuProfile, +// DefaultMountsFile: ctnrCfg.Containers.DefaultMountsFile, +// EngineMode: opts.EngineMode, +// EventsBackend: ctnrCfg.Engine.EventsLogger, +// FlagSet: opts.FlagSet, // TODO: deprecate +// HooksDir: append(ctnrCfg.Engine.HooksDir[:0:0], ctnrCfg.Engine.HooksDir...), +// Identities: append(opts.Identities[:0:0], opts.Identities...), +// IdentityFile: opts.IdentityFile, // TODO: deprecate +// IgnoreHosts: opts.IgnoreHosts, +// LogLevel: opts.LogLevel, +// MaxWorks: opts.MaxWorks, +// Namespace: ctnrCfg.Engine.Namespace, +// NetworkCmdPath: ctnrCfg.Engine.NetworkCmdPath, +// Port: opts.Port, +// RemoteConfigFilePath: opts.RemoteConfigFilePath, +// RemoteHost: opts.RemoteHost, // TODO: deprecate +// RemoteUserName: opts.RemoteUserName, // TODO: deprecate +// Root: opts.Root, +// Runroot: opts.Runroot, +// Runtime: opts.Runtime, +// StorageDriver: opts.StorageDriver, +// StorageOpts: append(opts.StorageOpts[:0:0], opts.StorageOpts...), +// Syslog: opts.Syslog, +// TmpDir: opts.TmpDir, +// Trace: opts.Trace, +// Uri: opts.Uri, +// VarlinkAddress: opts.VarlinkAddress, +// }, nil +// } diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index 7e455b969..5fdb9a8a6 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -3,11 +3,13 @@ package entities import ( "context" + "github.com/containers/common/pkg/config" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/specgen" ) type ContainerEngine interface { + Config(ctx context.Context) (*config.Config, error) ContainerAttach(ctx context.Context, nameOrId string, options AttachOptions) error ContainerCheckpoint(ctx context.Context, namesOrIds []string, options CheckpointOptions) ([]*CheckpointReport, error) ContainerCleanup(ctx context.Context, namesOrIds []string, options ContainerCleanupOptions) ([]*ContainerCleanupReport, error) @@ -17,9 +19,11 @@ type ContainerEngine interface { ContainerExec(ctx context.Context, nameOrId string, options ExecOptions) (int, error) ContainerExists(ctx context.Context, nameOrId string) (*BoolReport, error) ContainerExport(ctx context.Context, nameOrId string, options ContainerExportOptions) error + ContainerInit(ctx context.Context, namesOrIds []string, options ContainerInitOptions) ([]*ContainerInitReport, error) ContainerInspect(ctx context.Context, namesOrIds []string, options InspectOptions) ([]*ContainerInspectReport, error) ContainerKill(ctx context.Context, namesOrIds []string, options KillOptions) ([]*KillReport, error) ContainerList(ctx context.Context, options ContainerListOptions) ([]ListContainer, error) + ContainerMount(ctx context.Context, nameOrIds []string, options ContainerMountOptions) ([]*ContainerMountReport, error) ContainerPause(ctx context.Context, namesOrIds []string, options PauseUnPauseOptions) ([]*PauseUnpauseReport, error) ContainerLogs(ctx context.Context, containers []string, options ContainerLogsOptions) error ContainerRestart(ctx context.Context, namesOrIds []string, options RestartOptions) ([]*RestartReport, error) @@ -29,8 +33,10 @@ type ContainerEngine interface { ContainerStart(ctx context.Context, namesOrIds []string, options ContainerStartOptions) ([]*ContainerStartReport, error) ContainerStop(ctx context.Context, namesOrIds []string, options StopOptions) ([]*StopReport, error) ContainerTop(ctx context.Context, options TopOptions) (*StringSliceReport, error) + ContainerUnmount(ctx context.Context, nameOrIds []string, options ContainerUnmountOptions) ([]*ContainerUnmountReport, error) ContainerUnpause(ctx context.Context, namesOrIds []string, options PauseUnPauseOptions) ([]*PauseUnpauseReport, error) ContainerWait(ctx context.Context, namesOrIds []string, options WaitOptions) ([]WaitReport, error) + Events(ctx context.Context, opts EventsOptions) error HealthCheckRun(ctx context.Context, nameOrId string, options HealthCheckOptions) (*define.HealthCheckResults, error) Info(ctx context.Context) (*define.Info, error) PodCreate(ctx context.Context, opts PodCreateOptions) (*PodCreateReport, error) diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go index 16b96e9ef..3a051ab9b 100644 --- a/pkg/domain/entities/engine_image.go +++ b/pkg/domain/entities/engine_image.go @@ -2,9 +2,12 @@ package entities import ( "context" + + "github.com/containers/common/pkg/config" ) type ImageEngine interface { + Config(ctx context.Context) (*config.Config, error) Delete(ctx context.Context, nameOrId []string, opts ImageDeleteOptions) (*ImageDeleteReport, error) Diff(ctx context.Context, nameOrId string, options DiffOptions) (*DiffReport, error) Exists(ctx context.Context, nameOrId string) (*BoolReport, error) @@ -19,4 +22,5 @@ type ImageEngine interface { Save(ctx context.Context, nameOrId string, tags []string, options ImageSaveOptions) error Tag(ctx context.Context, nameOrId string, tags []string, options ImageTagOptions) error Untag(ctx context.Context, nameOrId string, tags []string, options ImageUntagOptions) error + Search(ctx context.Context, term string, opts ImageSearchOptions) ([]ImageSearchReport, error) } diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index bc8a34c13..78ebb8805 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -2,6 +2,7 @@ package entities import ( "net/url" + "time" "github.com/containers/image/v5/manifest" "github.com/containers/image/v5/types" @@ -99,12 +100,12 @@ type ImageDeleteReport struct { type ImageHistoryOptions struct{} type ImageHistoryLayer struct { - ID string `json:"Id"` - Created int64 `json:",omitempty"` - CreatedBy string `json:",omitempty"` - Tags []string `json:",omitempty"` - Size int64 `json:",omitempty"` - Comment string `json:",omitempty"` + ID string `json:"id"` + Created time.Time `json:"created,omitempty"` + CreatedBy string `json:",omitempty"` + Tags []string `json:"tags,omitempty"` + Size int64 `json:"size"` + Comment string `json:"comment,omitempty"` } type ImageHistoryReport struct { @@ -181,6 +182,37 @@ type ImagePushOptions struct { TLSVerify types.OptionalBool } +// ImageSearchOptions are the arguments for searching images. +type ImageSearchOptions struct { + // Authfile is the path to the authentication file. Ignored for remote + // calls. + Authfile string + // Filters for the search results. + Filters []string + // Limit the number of results. + Limit int + // NoTrunc will not truncate the output. + NoTrunc bool + // TLSVerify to enable/disable HTTPS and certificate verification. + TLSVerify types.OptionalBool +} + +// ImageSearchReport is the response from searching images. +type ImageSearchReport struct { + // Index is the image index (e.g., "docker.io" or "quay.io") + Index string + // Name is the canoncical name of the image (e.g., "docker.io/library/alpine"). + Name string + // Description of the image. + Description string + // Stars is the number of stars of the image. + Stars int + // Official indicates if it's an official image. + Official string + // Automated indicates if the image was created by an automated build. + Automated string +} + type ImageListOptions struct { All bool `json:"all" schema:"all"` Filter []string `json:"Filter,omitempty"` diff --git a/pkg/domain/entities/pods.go b/pkg/domain/entities/pods.go index cd2e79961..9ca8ff43c 100644 --- a/pkg/domain/entities/pods.go +++ b/pkg/domain/entities/pods.go @@ -1,6 +1,7 @@ package entities import ( + "strings" "time" "github.com/containers/libpod/libpod" @@ -121,7 +122,9 @@ func (p PodCreateOptions) ToPodSpecGen(s *specgen.PodSpecGenerator) { s.Hostname = p.Hostname s.Labels = p.Labels s.NoInfra = !p.Infra - s.InfraCommand = []string{p.InfraCommand} + if len(p.InfraCommand) > 0 { + s.InfraCommand = strings.Split(p.InfraCommand, " ") + } s.InfraImage = p.InfraImage s.SharedNamespaces = p.Share diff --git a/pkg/domain/entities/types.go b/pkg/domain/entities/types.go index 7e35957f4..91ae00764 100644 --- a/pkg/domain/entities/types.go +++ b/pkg/domain/entities/types.go @@ -3,6 +3,7 @@ package entities import ( "net" + "github.com/containers/libpod/libpod/events" "github.com/containers/libpod/pkg/specgen" "github.com/containers/storage/pkg/archive" "github.com/cri-o/ocicni/pkg/ocicni" @@ -62,3 +63,12 @@ type DiffOptions struct { type DiffReport struct { Changes []archive.Change } + +type EventsOptions struct { + FromStart bool + EventChan chan *events.Event + Filter []string + Stream bool + Since string + Until string +} diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index 7f8ec210b..f464df3ac 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -6,11 +6,13 @@ import ( "context" "fmt" "io/ioutil" + "os" "strconv" "strings" "sync" "github.com/containers/buildah" + "github.com/containers/common/pkg/config" "github.com/containers/image/v5/manifest" "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" @@ -21,9 +23,11 @@ import ( "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/domain/infra/abi/terminal" "github.com/containers/libpod/pkg/ps" + "github.com/containers/libpod/pkg/rootless" "github.com/containers/libpod/pkg/signal" "github.com/containers/libpod/pkg/specgen" "github.com/containers/libpod/pkg/specgen/generate" + "github.com/containers/storage" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -794,3 +798,104 @@ func (ic *ContainerEngine) ContainerCleanup(ctx context.Context, namesOrIds []st } return reports, nil } + +func (ic *ContainerEngine) ContainerInit(ctx context.Context, namesOrIds []string, options entities.ContainerInitOptions) ([]*entities.ContainerInitReport, error) { + var reports []*entities.ContainerInitReport + ctrs, err := getContainersByContext(options.All, options.Latest, namesOrIds, ic.Libpod) + if err != nil { + return nil, err + } + for _, ctr := range ctrs { + report := entities.ContainerInitReport{Id: ctr.ID()} + report.Err = ctr.Init(ctx) + reports = append(reports, &report) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerMount(ctx context.Context, nameOrIds []string, options entities.ContainerMountOptions) ([]*entities.ContainerMountReport, error) { + if os.Geteuid() != 0 { + if driver := ic.Libpod.StorageConfig().GraphDriverName; driver != "vfs" { + // Do not allow to mount a graphdriver that is not vfs if we are creating the userns as part + // of the mount command. + return nil, fmt.Errorf("cannot mount using driver %s in rootless mode", driver) + } + + became, ret, err := rootless.BecomeRootInUserNS("") + if err != nil { + return nil, err + } + if became { + os.Exit(ret) + } + } + var reports []*entities.ContainerMountReport + ctrs, err := getContainersByContext(options.All, options.Latest, nameOrIds, ic.Libpod) + if err != nil { + return nil, err + } + for _, ctr := range ctrs { + report := entities.ContainerMountReport{Id: ctr.ID()} + report.Path, report.Err = ctr.Mount() + reports = append(reports, &report) + } + if len(reports) > 0 { + return reports, nil + } + + // No containers were passed, so we send back what is mounted + ctrs, err = getContainersByContext(true, false, []string{}, ic.Libpod) + if err != nil { + return nil, err + } + for _, ctr := range ctrs { + mounted, path, err := ctr.Mounted() + if err != nil { + return nil, err + } + + if mounted { + reports = append(reports, &entities.ContainerMountReport{ + Id: ctr.ID(), + Name: ctr.Name(), + Path: path, + }) + } + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerUnmount(ctx context.Context, nameOrIds []string, options entities.ContainerUnmountOptions) ([]*entities.ContainerUnmountReport, error) { + var reports []*entities.ContainerUnmountReport + ctrs, err := getContainersByContext(options.All, options.Latest, nameOrIds, ic.Libpod) + if err != nil { + return nil, err + } + for _, ctr := range ctrs { + state, err := ctr.State() + if err != nil { + logrus.Debugf("Error umounting container %s state: %s", ctr.ID(), err.Error()) + continue + } + if state == define.ContainerStateRunning { + logrus.Debugf("Error umounting container %s, is running", ctr.ID()) + continue + } + + report := entities.ContainerUnmountReport{Id: ctr.ID()} + if err := ctr.Unmount(options.Force); err != nil { + if options.All && errors.Cause(err) == storage.ErrLayerNotMounted { + logrus.Debugf("Error umounting container %s, storage.ErrLayerNotMounted", ctr.ID()) + continue + } + report.Err = errors.Wrapf(err, "error unmounting container %s", ctr.ID()) + } + reports = append(reports, &report) + } + return reports, nil +} + +// GetConfig returns a copy of the configuration used by the runtime +func (ic *ContainerEngine) Config(_ context.Context) (*config.Config, error) { + return ic.Libpod.GetConfig() +} diff --git a/pkg/domain/infra/abi/events.go b/pkg/domain/infra/abi/events.go new file mode 100644 index 000000000..9540a5b96 --- /dev/null +++ b/pkg/domain/infra/abi/events.go @@ -0,0 +1,18 @@ +//+build ABISupport + +package abi + +import ( + "context" + + "github.com/containers/libpod/libpod/events" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/sirupsen/logrus" +) + +func (ic *ContainerEngine) Events(ctx context.Context, opts entities.EventsOptions) error { + readOpts := events.ReadOptions{FromStart: opts.FromStart, Stream: opts.Stream, Filters: opts.Filter, EventChannel: opts.EventChan, Since: opts.Since, Until: opts.Until} + err := ic.Libpod.Events(readOpts) + logrus.Error(err) + return err +} diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index 24ee596be..9467c14d4 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -9,6 +9,7 @@ import ( "os" "strings" + "github.com/containers/common/pkg/config" "github.com/containers/image/v5/docker" dockerarchive "github.com/containers/image/v5/docker/archive" "github.com/containers/image/v5/docker/reference" @@ -45,7 +46,9 @@ func (ir *ImageEngine) Delete(ctx context.Context, nameOrId []string, opts entit if err != nil { return &report, errors.Wrapf(err, "unable to query local images") } - + if len(targets) == 0 { + return &report, nil + } if len(targets) > 0 && len(targets) == len(previousTargets) { return &report, errors.New("unable to delete all images; re-run the rmi command again.") } @@ -142,7 +145,7 @@ func (ir *ImageEngine) History(ctx context.Context, nameOrId string, opts entiti func ToDomainHistoryLayer(layer *libpodImage.History) entities.ImageHistoryLayer { l := entities.ImageHistoryLayer{} l.ID = layer.ID - l.Created = layer.Created.Unix() + l.Created = *layer.Created l.CreatedBy = layer.CreatedBy copy(l.Tags, layer.Tags) l.Size = layer.Size @@ -394,8 +397,10 @@ func (ir *ImageEngine) Load(ctx context.Context, opts entities.ImageLoadOptions) if err != nil { return nil, errors.Wrap(err, "image loaded but no additional tags were created") } - if err := newImage.TagImage(opts.Name); err != nil { - return nil, errors.Wrapf(err, "error adding %q to image %q", opts.Name, newImage.InputName) + if len(opts.Name) > 0 { + if err := newImage.TagImage(fmt.Sprintf("%s:%s", opts.Name, opts.Tag)); err != nil { + return nil, errors.Wrapf(err, "error adding %q to image %q", opts.Name, newImage.InputName) + } } return &entities.ImageLoadReport{Name: name}, nil } @@ -423,3 +428,43 @@ func (ir *ImageEngine) Diff(_ context.Context, nameOrId string, _ entities.DiffO } return &entities.DiffReport{Changes: changes}, nil } + +func (ir *ImageEngine) Search(ctx context.Context, term string, opts entities.ImageSearchOptions) ([]entities.ImageSearchReport, error) { + filter, err := image.ParseSearchFilter(opts.Filters) + if err != nil { + return nil, err + } + + searchOpts := image.SearchOptions{ + Authfile: opts.Authfile, + Filter: *filter, + Limit: opts.Limit, + NoTrunc: opts.NoTrunc, + InsecureSkipTLSVerify: opts.TLSVerify, + } + + searchResults, err := image.SearchImages(term, searchOpts) + if err != nil { + return nil, err + } + + // Convert from image.SearchResults to entities.ImageSearchReport. We don't + // want to leak any low-level packages into the remote client, which + // requires converting. + reports := make([]entities.ImageSearchReport, len(searchResults)) + for i := range searchResults { + reports[i].Index = searchResults[i].Index + reports[i].Name = searchResults[i].Name + reports[i].Description = searchResults[i].Index + reports[i].Stars = searchResults[i].Stars + reports[i].Official = searchResults[i].Official + reports[i].Automated = searchResults[i].Automated + } + + return reports, nil +} + +// GetConfig returns a copy of the configuration used by the runtime +func (ir *ImageEngine) Config(_ context.Context) (*config.Config, error) { + return ir.Libpod.GetConfig() +} diff --git a/pkg/domain/infra/abi/pods.go b/pkg/domain/infra/abi/pods.go index c3e5d59bc..bb637de3e 100644 --- a/pkg/domain/infra/abi/pods.go +++ b/pkg/domain/infra/abi/pods.go @@ -5,13 +5,13 @@ package abi import ( "context" - lpfilters "github.com/containers/libpod/libpod/filters" - "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" + lpfilters "github.com/containers/libpod/libpod/filters" "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/signal" "github.com/containers/libpod/pkg/specgen" + "github.com/containers/libpod/pkg/specgen/generate" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -246,7 +246,7 @@ func (ic *ContainerEngine) PodRm(ctx context.Context, namesOrIds []string, optio func (ic *ContainerEngine) PodCreate(ctx context.Context, opts entities.PodCreateOptions) (*entities.PodCreateReport, error) { podSpec := specgen.NewPodSpecGenerator() opts.ToPodSpecGen(podSpec) - pod, err := podSpec.MakePod(ic.Libpod) + pod, err := generate.MakePod(podSpec, ic.Libpod) if err != nil { return nil, err } diff --git a/pkg/domain/infra/abi/system.go b/pkg/domain/infra/abi/system.go index 2df11cc1b..10872144b 100644 --- a/pkg/domain/infra/abi/system.go +++ b/pkg/domain/infra/abi/system.go @@ -4,17 +4,28 @@ package abi import ( "context" + "fmt" + "io/ioutil" "net" + "os" + "strconv" "strings" + "syscall" + "github.com/containers/common/pkg/config" "github.com/containers/libpod/libpod/define" api "github.com/containers/libpod/pkg/api/server" + "github.com/containers/libpod/pkg/cgroups" "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/rootless" + "github.com/containers/libpod/pkg/util" iopodman "github.com/containers/libpod/pkg/varlink" iopodmanAPI "github.com/containers/libpod/pkg/varlinkapi" + "github.com/containers/libpod/utils" "github.com/containers/libpod/version" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "github.com/spf13/cobra" "github.com/varlink/go/varlink" ) @@ -51,7 +62,6 @@ func (ic *ContainerEngine) RestService(_ context.Context, opts entities.ServiceO }() err = server.Serve() - logrus.Debugf("%d/%d Active connections/Total connections\n", server.ActiveConnections, server.TotalConnections) _ = listener.Close() return err } @@ -89,3 +99,146 @@ func (ic *ContainerEngine) VarlinkService(_ context.Context, opts entities.Servi } return nil } + +func (ic *ContainerEngine) SetupRootless(cmd *cobra.Command) error { + // do it only after podman has already re-execed and running with uid==0. + if os.Geteuid() == 0 { + ownsCgroup, err := cgroups.UserOwnsCurrentSystemdCgroup() + if err != nil { + logrus.Warnf("Failed to detect the owner for the current cgroup: %v", err) + } + if !ownsCgroup { + conf, err := ic.Config(context.Background()) + if err != nil { + return err + } + unitName := fmt.Sprintf("podman-%d.scope", os.Getpid()) + if err := utils.RunUnderSystemdScope(os.Getpid(), "user.slice", unitName); err != nil { + if conf.Engine.CgroupManager == config.SystemdCgroupsManager { + logrus.Warnf("Failed to add podman to systemd sandbox cgroup: %v", err) + } else { + logrus.Debugf("Failed to add podman to systemd sandbox cgroup: %v", err) + } + } + } + } + + if !executeCommandInUserNS(cmd) { + return nil + } + + pausePidPath, err := util.GetRootlessPauseProcessPidPath() + if err != nil { + return errors.Wrapf(err, "could not get pause process pid file path") + } + + became, ret, err := rootless.TryJoinPauseProcess(pausePidPath) + if err != nil { + return err + } + if became { + os.Exit(ret) + } + + // if there is no pid file, try to join existing containers, and create a pause process. + ctrs, err := ic.Libpod.GetRunningContainers() + if err != nil { + logrus.WithError(err).Fatal("") + } + + paths := []string{} + for _, ctr := range ctrs { + paths = append(paths, ctr.Config().ConmonPidFile) + } + + became, ret, err = rootless.TryJoinFromFilePaths(pausePidPath, true, paths) + if err := movePauseProcessToScope(); err != nil { + conf, err := ic.Config(context.Background()) + if err != nil { + return err + } + if conf.Engine.CgroupManager == config.SystemdCgroupsManager { + logrus.Warnf("Failed to add pause process to systemd sandbox cgroup: %v", err) + } else { + logrus.Debugf("Failed to add pause process to systemd sandbox cgroup: %v", err) + } + } + if err != nil { + logrus.WithError(err).Fatal("") + } + if became { + os.Exit(ret) + } + return nil +} + +// Most podman commands when run in rootless mode, need to be executed in the +// users usernamespace. This function is updated with a list of commands that +// should NOT be run within the user namespace. +func executeCommandInUserNS(cmd *cobra.Command) bool { + return os.Geteuid() == 0 + // if os.Geteuid() == 0 { + // return false + // } + // switch cmd { + // case _migrateCommand, + // _mountCommand, + // _renumberCommand, + // _searchCommand, + // _versionCommand: + // return false + // } + // return true +} + +func movePauseProcessToScope() error { + pausePidPath, err := util.GetRootlessPauseProcessPidPath() + if err != nil { + return errors.Wrapf(err, "could not get pause process pid file path") + } + + data, err := ioutil.ReadFile(pausePidPath) + if err != nil { + return errors.Wrapf(err, "cannot read pause pid file") + } + pid, err := strconv.ParseUint(string(data), 10, 0) + if err != nil { + return errors.Wrapf(err, "cannot parse pid file %s", pausePidPath) + } + + return utils.RunUnderSystemdScope(int(pid), "user.slice", "podman-pause.scope") +} + +func setRLimits() error { // nolint:deadcode,unused + rlimits := new(syscall.Rlimit) + rlimits.Cur = 1048576 + rlimits.Max = 1048576 + if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, rlimits); err != nil { + if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, rlimits); err != nil { + return errors.Wrapf(err, "error getting rlimits") + } + rlimits.Cur = rlimits.Max + if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, rlimits); err != nil { + return errors.Wrapf(err, "error setting new rlimits") + } + } + return nil +} + +func setUMask() { // nolint:deadcode,unused + // Be sure we can create directories with 0755 mode. + syscall.Umask(0022) +} + +// checkInput can be used to verify any of the globalopt values +func checkInput() error { // nolint:deadcode,unused + return nil +} + +// func getCNIPluginsDir() string { +// if rootless.IsRootless() { +// return "" +// } +// +// return registry.PodmanOptions.Network.CNIPluginDirs[0] +// } diff --git a/pkg/domain/infra/runtime_abi.go b/pkg/domain/infra/runtime_abi.go index f11026571..0dbcf2ad2 100644 --- a/pkg/domain/infra/runtime_abi.go +++ b/pkg/domain/infra/runtime_abi.go @@ -12,7 +12,7 @@ import ( ) // NewContainerEngine factory provides a libpod runtime for container-related operations -func NewContainerEngine(facts entities.EngineOptions) (entities.ContainerEngine, error) { +func NewContainerEngine(facts entities.PodmanConfig) (entities.ContainerEngine, error) { switch facts.EngineMode { case entities.ABIMode: r, err := NewLibpodRuntime(facts.FlagSet, facts) @@ -25,7 +25,7 @@ func NewContainerEngine(facts entities.EngineOptions) (entities.ContainerEngine, } // NewContainerEngine factory provides a libpod runtime for image-related operations -func NewImageEngine(facts entities.EngineOptions) (entities.ImageEngine, error) { +func NewImageEngine(facts entities.PodmanConfig) (entities.ImageEngine, error) { switch facts.EngineMode { case entities.ABIMode: r, err := NewLibpodImageRuntime(facts.FlagSet, facts) diff --git a/pkg/domain/infra/runtime_image_proxy.go b/pkg/domain/infra/runtime_image_proxy.go index befc66b9a..45c5425a3 100644 --- a/pkg/domain/infra/runtime_image_proxy.go +++ b/pkg/domain/infra/runtime_image_proxy.go @@ -12,7 +12,7 @@ import ( // ContainerEngine Image Proxy will be EOL'ed after podmanV2 is separated from libpod repo -func NewLibpodImageRuntime(flags *pflag.FlagSet, opts entities.EngineOptions) (entities.ImageEngine, error) { +func NewLibpodImageRuntime(flags *pflag.FlagSet, opts entities.PodmanConfig) (entities.ImageEngine, error) { r, err := GetRuntime(context.Background(), flags, opts) if err != nil { return nil, err diff --git a/pkg/domain/infra/runtime_libpod.go b/pkg/domain/infra/runtime_libpod.go index d59759707..9cf374e2e 100644 --- a/pkg/domain/infra/runtime_libpod.go +++ b/pkg/domain/infra/runtime_libpod.go @@ -1,3 +1,5 @@ +// build: ABISupport + package infra import ( @@ -22,68 +24,70 @@ type engineOpts struct { migrate bool noStore bool withFDS bool - flags entities.EngineOptions + config entities.PodmanConfig } // GetRuntimeMigrate gets a libpod runtime that will perform a migration of existing containers -func GetRuntimeMigrate(ctx context.Context, fs *flag.FlagSet, ef entities.EngineOptions, newRuntime string) (*libpod.Runtime, error) { +func GetRuntimeMigrate(ctx context.Context, fs *flag.FlagSet, cfg entities.PodmanConfig, newRuntime string) (*libpod.Runtime, error) { return getRuntime(ctx, fs, &engineOpts{ name: newRuntime, renumber: false, migrate: true, noStore: false, withFDS: true, - flags: ef, + config: cfg, }) } // GetRuntimeDisableFDs gets a libpod runtime that will disable sd notify -func GetRuntimeDisableFDs(ctx context.Context, fs *flag.FlagSet, ef entities.EngineOptions) (*libpod.Runtime, error) { +func GetRuntimeDisableFDs(ctx context.Context, fs *flag.FlagSet, cfg entities.PodmanConfig) (*libpod.Runtime, error) { return getRuntime(ctx, fs, &engineOpts{ renumber: false, migrate: false, noStore: false, withFDS: false, - flags: ef, + config: cfg, }) } // GetRuntimeRenumber gets a libpod runtime that will perform a lock renumber -func GetRuntimeRenumber(ctx context.Context, fs *flag.FlagSet, ef entities.EngineOptions) (*libpod.Runtime, error) { +func GetRuntimeRenumber(ctx context.Context, fs *flag.FlagSet, cfg entities.PodmanConfig) (*libpod.Runtime, error) { return getRuntime(ctx, fs, &engineOpts{ renumber: true, migrate: false, noStore: false, withFDS: true, - flags: ef, + config: cfg, }) } // GetRuntime generates a new libpod runtime configured by command line options -func GetRuntime(ctx context.Context, flags *flag.FlagSet, ef entities.EngineOptions) (*libpod.Runtime, error) { +func GetRuntime(ctx context.Context, flags *flag.FlagSet, cfg entities.PodmanConfig) (*libpod.Runtime, error) { return getRuntime(ctx, flags, &engineOpts{ renumber: false, migrate: false, noStore: false, withFDS: true, - flags: ef, + config: cfg, }) } // GetRuntimeNoStore generates a new libpod runtime configured by command line options -func GetRuntimeNoStore(ctx context.Context, fs *flag.FlagSet, ef entities.EngineOptions) (*libpod.Runtime, error) { +func GetRuntimeNoStore(ctx context.Context, fs *flag.FlagSet, cfg entities.PodmanConfig) (*libpod.Runtime, error) { return getRuntime(ctx, fs, &engineOpts{ renumber: false, migrate: false, noStore: true, withFDS: true, - flags: ef, + config: cfg, }) } func getRuntime(ctx context.Context, fs *flag.FlagSet, opts *engineOpts) (*libpod.Runtime, error) { options := []libpod.RuntimeOption{} storageOpts := storage.StoreOptions{} + cfg := opts.config + storageSet := false uidmapFlag := fs.Lookup("uidmap") @@ -109,25 +113,25 @@ func getRuntime(ctx context.Context, fs *flag.FlagSet, opts *engineOpts) (*libpo if fs.Changed("root") { storageSet = true - storageOpts.GraphRoot = opts.flags.Root + storageOpts.GraphRoot = cfg.Engine.StaticDir } if fs.Changed("runroot") { storageSet = true - storageOpts.RunRoot = opts.flags.Runroot + storageOpts.RunRoot = cfg.Runroot } if len(storageOpts.RunRoot) > 50 { return nil, errors.New("the specified runroot is longer than 50 characters") } if fs.Changed("storage-driver") { storageSet = true - storageOpts.GraphDriverName = opts.flags.StorageDriver + storageOpts.GraphDriverName = cfg.StorageDriver // Overriding the default storage driver caused GraphDriverOptions from storage.conf to be ignored storageOpts.GraphDriverOptions = []string{} } // This should always be checked after storage-driver is checked - if len(opts.flags.StorageOpts) > 0 { + if len(cfg.StorageOpts) > 0 { storageSet = true - storageOpts.GraphDriverOptions = opts.flags.StorageOpts + storageOpts.GraphDriverOptions = cfg.StorageOpts } if opts.migrate { options = append(options, libpod.WithMigrate()) @@ -151,30 +155,30 @@ func getRuntime(ctx context.Context, fs *flag.FlagSet, opts *engineOpts) (*libpo // TODO CLI flags for image config? // TODO CLI flag for signature policy? - if len(opts.flags.Namespace) > 0 { - options = append(options, libpod.WithNamespace(opts.flags.Namespace)) + if len(cfg.Engine.Namespace) > 0 { + options = append(options, libpod.WithNamespace(cfg.Engine.Namespace)) } if fs.Changed("runtime") { - options = append(options, libpod.WithOCIRuntime(opts.flags.Runtime)) + options = append(options, libpod.WithOCIRuntime(cfg.RuntimePath)) } if fs.Changed("conmon") { - options = append(options, libpod.WithConmonPath(opts.flags.ConmonPath)) + options = append(options, libpod.WithConmonPath(cfg.ConmonPath)) } if fs.Changed("tmpdir") { - options = append(options, libpod.WithTmpDir(opts.flags.TmpDir)) + options = append(options, libpod.WithTmpDir(cfg.Engine.TmpDir)) } if fs.Changed("network-cmd-path") { - options = append(options, libpod.WithNetworkCmdPath(opts.flags.NetworkCmdPath)) + options = append(options, libpod.WithNetworkCmdPath(cfg.Engine.NetworkCmdPath)) } if fs.Changed("events-backend") { - options = append(options, libpod.WithEventsLogger(opts.flags.EventsBackend)) + options = append(options, libpod.WithEventsLogger(cfg.Engine.EventsLogger)) } if fs.Changed("cgroup-manager") { - options = append(options, libpod.WithCgroupManager(opts.flags.CGroupManager)) + options = append(options, libpod.WithCgroupManager(cfg.Engine.CgroupManager)) } else { unified, err := cgroups.IsCgroup2UnifiedMode() if err != nil { @@ -189,13 +193,13 @@ func getRuntime(ctx context.Context, fs *flag.FlagSet, opts *engineOpts) (*libpo // TODO flag to set libpod tmp dir? if fs.Changed("cni-config-dir") { - options = append(options, libpod.WithCNIConfigDir(opts.flags.CniConfigDir)) + options = append(options, libpod.WithCNIConfigDir(cfg.Network.NetworkConfigDir)) } if fs.Changed("default-mounts-file") { - options = append(options, libpod.WithDefaultMountsFile(opts.flags.DefaultMountsFile)) + options = append(options, libpod.WithDefaultMountsFile(cfg.Containers.DefaultMountsFile)) } if fs.Changed("hooks-dir") { - options = append(options, libpod.WithHooksDir(opts.flags.HooksDir...)) + options = append(options, libpod.WithHooksDir(cfg.Engine.HooksDir...)) } // TODO flag to set CNI plugins dir? diff --git a/pkg/domain/infra/runtime_proxy.go b/pkg/domain/infra/runtime_proxy.go index 2e38c74b9..18f716ea0 100644 --- a/pkg/domain/infra/runtime_proxy.go +++ b/pkg/domain/infra/runtime_proxy.go @@ -12,7 +12,7 @@ import ( // ContainerEngine Proxy will be EOL'ed after podmanV2 is separated from libpod repo -func NewLibpodRuntime(flags *flag.FlagSet, opts entities.EngineOptions) (entities.ContainerEngine, error) { +func NewLibpodRuntime(flags *flag.FlagSet, opts entities.PodmanConfig) (entities.ContainerEngine, error) { r, err := GetRuntime(context.Background(), flags, opts) if err != nil { return nil, err diff --git a/pkg/domain/infra/runtime_tunnel.go b/pkg/domain/infra/runtime_tunnel.go index dc04b4e53..129fdeb2c 100644 --- a/pkg/domain/infra/runtime_tunnel.go +++ b/pkg/domain/infra/runtime_tunnel.go @@ -11,7 +11,7 @@ import ( "github.com/containers/libpod/pkg/domain/infra/tunnel" ) -func NewContainerEngine(facts entities.EngineOptions) (entities.ContainerEngine, error) { +func NewContainerEngine(facts entities.PodmanConfig) (entities.ContainerEngine, error) { switch facts.EngineMode { case entities.ABIMode: return nil, fmt.Errorf("direct runtime not supported") @@ -23,7 +23,7 @@ func NewContainerEngine(facts entities.EngineOptions) (entities.ContainerEngine, } // NewImageEngine factory provides a libpod runtime for image-related operations -func NewImageEngine(facts entities.EngineOptions) (entities.ImageEngine, error) { +func NewImageEngine(facts entities.PodmanConfig) (entities.ImageEngine, error) { switch facts.EngineMode { case entities.ABIMode: return nil, fmt.Errorf("direct image runtime not supported") diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index 2bc3a1914..05b62efcf 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -5,6 +5,7 @@ import ( "io" "os" + "github.com/containers/common/pkg/config" "github.com/containers/image/v5/docker/reference" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/bindings/containers" @@ -242,7 +243,7 @@ func (ic *ContainerEngine) ContainerCheckpoint(ctx context.Context, namesOrIds [ } // narrow the list to running only for _, c := range allCtrs { - if c.ContainerState == define.ContainerStateRunning.String() { + if c.State == define.ContainerStateRunning.String() { ctrs = append(ctrs, c) } } @@ -276,7 +277,7 @@ func (ic *ContainerEngine) ContainerRestore(ctx context.Context, namesOrIds []st } // narrow the list to exited only for _, c := range allCtrs { - if c.ContainerState == define.ContainerStateExited.String() { + if c.State == define.ContainerStateExited.String() { ctrs = append(ctrs, c) } } @@ -338,3 +339,31 @@ func (ic *ContainerEngine) ContainerDiff(ctx context.Context, nameOrId string, _ func (ic *ContainerEngine) ContainerCleanup(ctx context.Context, namesOrIds []string, options entities.ContainerCleanupOptions) ([]*entities.ContainerCleanupReport, error) { return nil, errors.New("not implemented") } + +func (ic *ContainerEngine) ContainerInit(ctx context.Context, namesOrIds []string, options entities.ContainerInitOptions) ([]*entities.ContainerInitReport, error) { + var reports []*entities.ContainerInitReport + ctrs, err := getContainersByContext(ic.ClientCxt, options.All, namesOrIds) + if err != nil { + return nil, err + } + for _, ctr := range ctrs { + err := containers.ContainerInit(ic.ClientCxt, ctr.ID) + reports = append(reports, &entities.ContainerInitReport{ + Err: err, + Id: ctr.ID, + }) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerMount(ctx context.Context, nameOrIds []string, options entities.ContainerMountOptions) ([]*entities.ContainerMountReport, error) { + return nil, errors.New("mounting containers is not supported for remote clients") +} + +func (ic *ContainerEngine) ContainerUnmount(ctx context.Context, nameOrIds []string, options entities.ContainerUnmountOptions) ([]*entities.ContainerUnmountReport, error) { + return nil, errors.New("unmounting containers is not supported for remote clients") +} + +func (ic *ContainerEngine) Config(_ context.Context) (*config.Config, error) { + return config.Default() +} diff --git a/pkg/domain/infra/tunnel/events.go b/pkg/domain/infra/tunnel/events.go new file mode 100644 index 000000000..46d88341a --- /dev/null +++ b/pkg/domain/infra/tunnel/events.go @@ -0,0 +1,31 @@ +package tunnel + +import ( + "context" + "strings" + + "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/bindings/system" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" +) + +func (ic *ContainerEngine) Events(ctx context.Context, opts entities.EventsOptions) error { + filters := make(map[string][]string) + if len(opts.Filter) > 0 { + for _, filter := range opts.Filter { + split := strings.Split(filter, "=") + if len(split) < 2 { + return errors.Errorf("invalid filter %q", filter) + } + filters[split[0]] = append(filters[split[0]], strings.Join(split[1:], "=")) + } + } + binChan := make(chan handlers.Event) + go func() { + for e := range binChan { + opts.EventChan <- e.ToLibpodEvent() + } + }() + return system.Events(ic.ClientCxt, binChan, nil, &opts.Since, &opts.Until, filters) +} diff --git a/pkg/domain/infra/tunnel/helpers.go b/pkg/domain/infra/tunnel/helpers.go index 4d7e45897..682d60d6a 100644 --- a/pkg/domain/infra/tunnel/helpers.go +++ b/pkg/domain/infra/tunnel/helpers.go @@ -30,7 +30,7 @@ func getContainersByContext(contextWithConnection context.Context, all bool, nam for _, id := range namesOrIds { var found bool for _, con := range c { - if id == con.ID || strings.HasPrefix(con.ID, id) || util.StringInSlice(id, con.ContainerNames) { + if id == con.ID || strings.HasPrefix(con.ID, id) || util.StringInSlice(id, con.Names) { cons = append(cons, con) found = true break diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go index 66abd7f47..7d40e0327 100644 --- a/pkg/domain/infra/tunnel/images.go +++ b/pkg/domain/infra/tunnel/images.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "os" + "github.com/containers/common/pkg/config" "github.com/containers/image/v5/docker/reference" images "github.com/containers/libpod/pkg/bindings/images" "github.com/containers/libpod/pkg/domain/entities" @@ -250,3 +251,11 @@ func (ir *ImageEngine) Diff(ctx context.Context, nameOrId string, _ entities.Dif } return &entities.DiffReport{Changes: changes}, nil } + +func (ir *ImageEngine) Search(ctx context.Context, term string, opts entities.ImageSearchOptions) ([]entities.ImageSearchReport, error) { + return images.Search(ir.ClientCxt, term, opts) +} + +func (ir *ImageEngine) Config(_ context.Context) (*config.Config, error) { + return config.Default() +} diff --git a/pkg/ps/ps.go b/pkg/ps/ps.go index 9217fa595..58fcc2c21 100644 --- a/pkg/ps/ps.go +++ b/pkg/ps/ps.go @@ -148,23 +148,23 @@ func ListContainerBatch(rt *libpod.Runtime, ctr *libpod.Container, opts entities } ps := entities.ListContainer{ - Cmd: conConfig.Command, - Created: conConfig.CreatedTime.Unix(), - Exited: exited, - ExitCode: exitCode, - ExitedAt: exitedTime.Unix(), - ID: conConfig.ID, - Image: conConfig.RootfsImageName, - IsInfra: conConfig.IsInfra, - Labels: conConfig.Labels, - Mounts: ctr.UserVolumes(), - ContainerNames: []string{conConfig.Name}, - Pid: pid, - Pod: conConfig.Pod, - PortMappings: conConfig.PortMappings, - ContainerSize: size, - StartedAt: startedTime.Unix(), - ContainerState: conState.String(), + Command: conConfig.Command, + Created: conConfig.CreatedTime.Unix(), + Exited: exited, + ExitCode: exitCode, + ExitedAt: exitedTime.Unix(), + ID: conConfig.ID, + Image: conConfig.RootfsImageName, + IsInfra: conConfig.IsInfra, + Labels: conConfig.Labels, + Mounts: ctr.UserVolumes(), + Names: []string{conConfig.Name}, + Pid: pid, + Pod: conConfig.Pod, + Ports: conConfig.PortMappings, + Size: size, + StartedAt: startedTime.Unix(), + State: conState.String(), } if opts.Pod && len(conConfig.Pod) > 0 { pod, err := rt.GetPod(conConfig.Pod) diff --git a/pkg/rootless/rootless_linux.c b/pkg/rootless/rootless_linux.c index 6643bfbbf..da52a7217 100644 --- a/pkg/rootless/rootless_linux.c +++ b/pkg/rootless/rootless_linux.c @@ -288,6 +288,7 @@ static void __attribute__((constructor)) init() char *cwd = getcwd (NULL, 0); char uid_fmt[16]; char gid_fmt[16]; + size_t len; if (cwd == NULL) { @@ -295,13 +296,13 @@ static void __attribute__((constructor)) init() _exit (EXIT_FAILURE); } - if (strlen (xdg_runtime_dir) >= PATH_MAX - strlen (suffix)) + len = snprintf (path, PATH_MAX, "%s%s", xdg_runtime_dir, suffix); + if (len >= PATH_MAX) { fprintf (stderr, "invalid value for XDG_RUNTIME_DIR: %s", strerror (ENAMETOOLONG)); exit (EXIT_FAILURE); } - sprintf (path, "%s%s", xdg_runtime_dir, suffix); fd = open (path, O_RDONLY); if (fd < 0) { diff --git a/pkg/signal/signal_linux.go b/pkg/signal/signal_linux.go index 76ab16ec7..e6e0f1ca1 100644 --- a/pkg/signal/signal_linux.go +++ b/pkg/signal/signal_linux.go @@ -129,14 +129,15 @@ func StopCatch(sigc chan os.Signal) { // ParseSignalNameOrNumber translates a string to a valid syscall signal. Input // can be a name or number representation i.e. "KILL" "9" func ParseSignalNameOrNumber(rawSignal string) (syscall.Signal, error) { - s, err := ParseSignal(rawSignal) + basename := strings.TrimPrefix(rawSignal, "-") + s, err := ParseSignal(basename) if err == nil { return s, nil } for k, v := range signalMap { - if k == strings.ToUpper(rawSignal) { + if k == strings.ToUpper(basename) { return v, nil } } - return -1, fmt.Errorf("invalid signal: %s", rawSignal) + return -1, fmt.Errorf("invalid signal: %s", basename) } diff --git a/pkg/specgen/config_linux_nocgo.go b/pkg/specgen/config_linux_nocgo.go deleted file mode 100644 index fc0c58c37..000000000 --- a/pkg/specgen/config_linux_nocgo.go +++ /dev/null @@ -1,11 +0,0 @@ -// +build linux,!cgo - -package specgen - -import ( - spec "github.com/opencontainers/runtime-spec/specs-go" -) - -func (s *SpecGenerator) getSeccompConfig(configSpec *spec.Spec) (*spec.LinuxSeccomp, error) { - return nil, nil -} diff --git a/pkg/specgen/config_linux_cgo.go b/pkg/specgen/generate/config_linux_cgo.go index ef6c6e951..b06ef5c9a 100644 --- a/pkg/specgen/config_linux_cgo.go +++ b/pkg/specgen/generate/config_linux_cgo.go @@ -1,6 +1,6 @@ // +build linux,cgo -package specgen +package generate import ( "context" @@ -8,13 +8,14 @@ import ( "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/seccomp" + "github.com/containers/libpod/pkg/specgen" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" goSeccomp "github.com/seccomp/containers-golang" "github.com/sirupsen/logrus" ) -func (s *SpecGenerator) getSeccompConfig(configSpec *spec.Spec, img *image.Image) (*spec.LinuxSeccomp, error) { +func getSeccompConfig(s *specgen.SpecGenerator, configSpec *spec.Spec, img *image.Image) (*spec.LinuxSeccomp, error) { var seccompConfig *spec.LinuxSeccomp var err error scp, err := seccomp.LookupPolicy(s.SeccompPolicy) diff --git a/pkg/specgen/generate/config_linux_nocgo.go b/pkg/specgen/generate/config_linux_nocgo.go new file mode 100644 index 000000000..fc8ed206d --- /dev/null +++ b/pkg/specgen/generate/config_linux_nocgo.go @@ -0,0 +1,14 @@ +// +build linux,!cgo + +package generate + +import ( + "errors" + + "github.com/containers/libpod/pkg/specgen" + spec "github.com/opencontainers/runtime-spec/specs-go" +) + +func (s *specgen.SpecGenerator) getSeccompConfig(configSpec *spec.Spec) (*spec.LinuxSeccomp, error) { + return nil, errors.New("not implemented") +} diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go index aad59a861..264e0ff8e 100644 --- a/pkg/specgen/generate/container_create.go +++ b/pkg/specgen/generate/container_create.go @@ -40,7 +40,7 @@ func MakeContainer(rt *libpod.Runtime, s *specgen.SpecGenerator) (*libpod.Contai options = append(options, libpod.WithRootFSFromImage(newImage.ID(), s.Image, s.RawImageName)) - runtimeSpec, err := s.ToOCISpec(rt, newImage) + runtimeSpec, err := SpecGenToOCI(s, rt, newImage) if err != nil { return nil, err } @@ -80,7 +80,15 @@ func createContainerOptions(rt *libpod.Runtime, s *specgen.SpecGenerator) ([]lib options = append(options, libpod.WithUserVolumes(destinations)) if len(s.Volumes) != 0 { - options = append(options, libpod.WithNamedVolumes(s.Volumes)) + var volumes []*libpod.ContainerNamedVolume + for _, v := range s.Volumes { + volumes = append(volumes, &libpod.ContainerNamedVolume{ + Name: v.Name, + Dest: v.Dest, + Options: v.Options, + }) + } + options = append(options, libpod.WithNamedVolumes(volumes)) } if len(s.Command) != 0 { @@ -115,7 +123,7 @@ func createContainerOptions(rt *libpod.Runtime, s *specgen.SpecGenerator) ([]lib options = append(options, libpod.WithPrivileged(s.Privileged)) // Get namespace related options - namespaceOptions, err := s.GenerateNamespaceContainerOpts(rt) + namespaceOptions, err := GenerateNamespaceContainerOpts(s, rt) if err != nil { return nil, err } diff --git a/pkg/specgen/generate/namespaces.go b/pkg/specgen/generate/namespaces.go new file mode 100644 index 000000000..cdd7d86da --- /dev/null +++ b/pkg/specgen/generate/namespaces.go @@ -0,0 +1,417 @@ +package generate + +import ( + "os" + + "github.com/containers/common/pkg/capabilities" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/specgen" + "github.com/cri-o/ocicni/pkg/ocicni" + spec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/opencontainers/runtime-tools/generate" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +func GenerateNamespaceContainerOpts(s *specgen.SpecGenerator, rt *libpod.Runtime) ([]libpod.CtrCreateOption, error) { + var portBindings []ocicni.PortMapping + options := make([]libpod.CtrCreateOption, 0) + + // Cgroups + switch { + case s.CgroupNS.IsPrivate(): + ns := s.CgroupNS.Value + if _, err := os.Stat(ns); err != nil { + return nil, err + } + case s.CgroupNS.IsContainer(): + connectedCtr, err := rt.LookupContainer(s.CgroupNS.Value) + if err != nil { + return nil, errors.Wrapf(err, "container %q not found", s.CgroupNS.Value) + } + options = append(options, libpod.WithCgroupNSFrom(connectedCtr)) + // TODO + //default: + // return nil, errors.New("cgroup name only supports private and container") + } + + if s.CgroupParent != "" { + options = append(options, libpod.WithCgroupParent(s.CgroupParent)) + } + + if s.CgroupsMode != "" { + options = append(options, libpod.WithCgroupsMode(s.CgroupsMode)) + } + + // ipc + switch { + case s.IpcNS.IsHost(): + options = append(options, libpod.WithShmDir("/dev/shm")) + case s.IpcNS.IsContainer(): + connectedCtr, err := rt.LookupContainer(s.IpcNS.Value) + if err != nil { + return nil, errors.Wrapf(err, "container %q not found", s.IpcNS.Value) + } + options = append(options, libpod.WithIPCNSFrom(connectedCtr)) + options = append(options, libpod.WithShmDir(connectedCtr.ShmDir())) + } + + // pid + if s.PidNS.IsContainer() { + connectedCtr, err := rt.LookupContainer(s.PidNS.Value) + if err != nil { + return nil, errors.Wrapf(err, "container %q not found", s.PidNS.Value) + } + options = append(options, libpod.WithPIDNSFrom(connectedCtr)) + } + + // uts + switch { + case s.UtsNS.IsPod(): + connectedPod, err := rt.LookupPod(s.UtsNS.Value) + if err != nil { + return nil, errors.Wrapf(err, "pod %q not found", s.UtsNS.Value) + } + options = append(options, libpod.WithUTSNSFromPod(connectedPod)) + case s.UtsNS.IsContainer(): + connectedCtr, err := rt.LookupContainer(s.UtsNS.Value) + if err != nil { + return nil, errors.Wrapf(err, "container %q not found", s.UtsNS.Value) + } + + options = append(options, libpod.WithUTSNSFrom(connectedCtr)) + } + + if s.UseImageHosts { + options = append(options, libpod.WithUseImageHosts()) + } else if len(s.HostAdd) > 0 { + options = append(options, libpod.WithHosts(s.HostAdd)) + } + + // User + + switch { + case s.UserNS.IsPath(): + ns := s.UserNS.Value + if ns == "" { + return nil, errors.Errorf("invalid empty user-defined user namespace") + } + _, err := os.Stat(ns) + if err != nil { + return nil, err + } + if s.IDMappings != nil { + options = append(options, libpod.WithIDMappings(*s.IDMappings)) + } + case s.UserNS.IsContainer(): + connectedCtr, err := rt.LookupContainer(s.UserNS.Value) + if err != nil { + return nil, errors.Wrapf(err, "container %q not found", s.UserNS.Value) + } + options = append(options, libpod.WithUserNSFrom(connectedCtr)) + default: + if s.IDMappings != nil { + options = append(options, libpod.WithIDMappings(*s.IDMappings)) + } + } + + options = append(options, libpod.WithUser(s.User)) + options = append(options, libpod.WithGroups(s.Groups)) + + if len(s.PortMappings) > 0 { + portBindings = s.PortMappings + } + + switch { + case s.NetNS.IsPath(): + ns := s.NetNS.Value + if ns == "" { + return nil, errors.Errorf("invalid empty user-defined network namespace") + } + _, err := os.Stat(ns) + if err != nil { + return nil, err + } + case s.NetNS.IsContainer(): + connectedCtr, err := rt.LookupContainer(s.NetNS.Value) + if err != nil { + return nil, errors.Wrapf(err, "container %q not found", s.NetNS.Value) + } + options = append(options, libpod.WithNetNSFrom(connectedCtr)) + case !s.NetNS.IsHost() && s.NetNS.NSMode != specgen.NoNetwork: + postConfigureNetNS := !s.UserNS.IsHost() + options = append(options, libpod.WithNetNS(portBindings, postConfigureNetNS, string(s.NetNS.NSMode), s.CNINetworks)) + } + + if len(s.DNSSearch) > 0 { + options = append(options, libpod.WithDNSSearch(s.DNSSearch)) + } + if len(s.DNSServer) > 0 { + // TODO I'm not sure how we are going to handle this given the input + if len(s.DNSServer) == 1 { //&& strings.ToLower(s.DNSServer[0].) == "none" { + options = append(options, libpod.WithUseImageResolvConf()) + } else { + var dnsServers []string + for _, d := range s.DNSServer { + dnsServers = append(dnsServers, d.String()) + } + options = append(options, libpod.WithDNS(dnsServers)) + } + } + if len(s.DNSOption) > 0 { + options = append(options, libpod.WithDNSOption(s.DNSOption)) + } + if s.StaticIP != nil { + options = append(options, libpod.WithStaticIP(*s.StaticIP)) + } + + if s.StaticMAC != nil { + options = append(options, libpod.WithStaticMAC(*s.StaticMAC)) + } + return options, nil +} + +func pidConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator) error { + if s.PidNS.IsPath() { + return g.AddOrReplaceLinuxNamespace(string(spec.PIDNamespace), s.PidNS.Value) + } + if s.PidNS.IsHost() { + return g.RemoveLinuxNamespace(string(spec.PIDNamespace)) + } + if s.PidNS.IsContainer() { + logrus.Debugf("using container %s pidmode", s.PidNS.Value) + } + if s.PidNS.IsPod() { + logrus.Debug("using pod pidmode") + } + return nil +} + +func utsConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator, runtime *libpod.Runtime) error { + hostname := s.Hostname + var err error + if hostname == "" { + switch { + case s.UtsNS.IsContainer(): + utsCtr, err := runtime.LookupContainer(s.UtsNS.Value) + if err != nil { + return errors.Wrapf(err, "unable to retrieve hostname from dependency container %s", s.UtsNS.Value) + } + hostname = utsCtr.Hostname() + case s.NetNS.IsHost() || s.UtsNS.IsHost(): + hostname, err = os.Hostname() + if err != nil { + return errors.Wrap(err, "unable to retrieve hostname of the host") + } + default: + logrus.Debug("No hostname set; container's hostname will default to runtime default") + } + } + g.RemoveHostname() + if s.Hostname != "" || !s.UtsNS.IsHost() { + // Set the hostname in the OCI configuration only + // if specified by the user or if we are creating + // a new UTS namespace. + g.SetHostname(hostname) + } + g.AddProcessEnv("HOSTNAME", hostname) + + if s.UtsNS.IsPath() { + return g.AddOrReplaceLinuxNamespace(string(spec.UTSNamespace), s.UtsNS.Value) + } + if s.UtsNS.IsHost() { + return g.RemoveLinuxNamespace(string(spec.UTSNamespace)) + } + if s.UtsNS.IsContainer() { + logrus.Debugf("using container %s utsmode", s.UtsNS.Value) + } + return nil +} + +func ipcConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator) error { + if s.IpcNS.IsPath() { + return g.AddOrReplaceLinuxNamespace(string(spec.IPCNamespace), s.IpcNS.Value) + } + if s.IpcNS.IsHost() { + return g.RemoveLinuxNamespace(s.IpcNS.Value) + } + if s.IpcNS.IsContainer() { + logrus.Debugf("Using container %s ipcmode", s.IpcNS.Value) + } + return nil +} + +func cgroupConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator) error { + if s.CgroupNS.IsPath() { + return g.AddOrReplaceLinuxNamespace(string(spec.CgroupNamespace), s.CgroupNS.Value) + } + if s.CgroupNS.IsHost() { + return g.RemoveLinuxNamespace(s.CgroupNS.Value) + } + if s.CgroupNS.IsPrivate() { + return g.AddOrReplaceLinuxNamespace(string(spec.CgroupNamespace), "") + } + if s.CgroupNS.IsContainer() { + logrus.Debugf("Using container %s cgroup mode", s.CgroupNS.Value) + } + return nil +} + +func networkConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator) error { + switch { + case s.NetNS.IsHost(): + logrus.Debug("Using host netmode") + if err := g.RemoveLinuxNamespace(string(spec.NetworkNamespace)); err != nil { + return err + } + + case s.NetNS.NSMode == specgen.NoNetwork: + logrus.Debug("Using none netmode") + case s.NetNS.NSMode == specgen.Bridge: + logrus.Debug("Using bridge netmode") + case s.NetNS.IsContainer(): + logrus.Debugf("using container %s netmode", s.NetNS.Value) + case s.NetNS.IsPath(): + logrus.Debug("Using ns netmode") + if err := g.AddOrReplaceLinuxNamespace(string(spec.NetworkNamespace), s.NetNS.Value); err != nil { + return err + } + case s.NetNS.IsPod(): + logrus.Debug("Using pod netmode, unless pod is not sharing") + case s.NetNS.NSMode == specgen.Slirp: + logrus.Debug("Using slirp4netns netmode") + default: + return errors.Errorf("unknown network mode") + } + + if g.Config.Annotations == nil { + g.Config.Annotations = make(map[string]string) + } + + if s.PublishImagePorts { + g.Config.Annotations[libpod.InspectAnnotationPublishAll] = libpod.InspectResponseTrue + } else { + g.Config.Annotations[libpod.InspectAnnotationPublishAll] = libpod.InspectResponseFalse + } + + return nil +} + +func userConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator) error { + if s.UserNS.IsPath() { + if err := g.AddOrReplaceLinuxNamespace(string(spec.UserNamespace), s.UserNS.Value); err != nil { + return err + } + // runc complains if no mapping is specified, even if we join another ns. So provide a dummy mapping + g.AddLinuxUIDMapping(uint32(0), uint32(0), uint32(1)) + g.AddLinuxGIDMapping(uint32(0), uint32(0), uint32(1)) + } + + if s.IDMappings != nil { + if (len(s.IDMappings.UIDMap) > 0 || len(s.IDMappings.GIDMap) > 0) && !s.UserNS.IsHost() { + if err := g.AddOrReplaceLinuxNamespace(string(spec.UserNamespace), ""); err != nil { + return err + } + } + for _, uidmap := range s.IDMappings.UIDMap { + g.AddLinuxUIDMapping(uint32(uidmap.HostID), uint32(uidmap.ContainerID), uint32(uidmap.Size)) + } + for _, gidmap := range s.IDMappings.GIDMap { + g.AddLinuxGIDMapping(uint32(gidmap.HostID), uint32(gidmap.ContainerID), uint32(gidmap.Size)) + } + } + return nil +} + +func securityConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator, newImage *image.Image) error { + // HANDLE CAPABILITIES + // NOTE: Must happen before SECCOMP + if s.Privileged { + g.SetupPrivileged(true) + } + + useNotRoot := func(user string) bool { + if user == "" || user == "root" || user == "0" { + return false + } + return true + } + configSpec := g.Config + var err error + var caplist []string + bounding := configSpec.Process.Capabilities.Bounding + if useNotRoot(s.User) { + configSpec.Process.Capabilities.Bounding = caplist + } + caplist, err = capabilities.MergeCapabilities(configSpec.Process.Capabilities.Bounding, s.CapAdd, s.CapDrop) + if err != nil { + return err + } + + configSpec.Process.Capabilities.Bounding = caplist + configSpec.Process.Capabilities.Permitted = caplist + configSpec.Process.Capabilities.Inheritable = caplist + configSpec.Process.Capabilities.Effective = caplist + configSpec.Process.Capabilities.Ambient = caplist + if useNotRoot(s.User) { + caplist, err = capabilities.MergeCapabilities(bounding, s.CapAdd, s.CapDrop) + if err != nil { + return err + } + } + configSpec.Process.Capabilities.Bounding = caplist + + // HANDLE SECCOMP + if s.SeccompProfilePath != "unconfined" { + seccompConfig, err := getSeccompConfig(s, configSpec, newImage) + if err != nil { + return err + } + configSpec.Linux.Seccomp = seccompConfig + } + + // Clear default Seccomp profile from Generator for privileged containers + if s.SeccompProfilePath == "unconfined" || s.Privileged { + configSpec.Linux.Seccomp = nil + } + + g.SetRootReadonly(s.ReadOnlyFilesystem) + for sysctlKey, sysctlVal := range s.Sysctl { + g.AddLinuxSysctl(sysctlKey, sysctlVal) + } + + return nil +} + +// GetNamespaceOptions transforms a slice of kernel namespaces +// into a slice of pod create options. Currently, not all +// kernel namespaces are supported, and they will be returned in an error +func GetNamespaceOptions(ns []string) ([]libpod.PodCreateOption, error) { + var options []libpod.PodCreateOption + var erroredOptions []libpod.PodCreateOption + for _, toShare := range ns { + switch toShare { + case "cgroup": + options = append(options, libpod.WithPodCgroups()) + case "net": + options = append(options, libpod.WithPodNet()) + case "mnt": + return erroredOptions, errors.Errorf("Mount sharing functionality not supported on pod level") + case "pid": + options = append(options, libpod.WithPodPID()) + case "user": + return erroredOptions, errors.Errorf("User sharing functionality not supported on pod level") + case "ipc": + options = append(options, libpod.WithPodIPC()) + case "uts": + options = append(options, libpod.WithPodUTS()) + case "": + case "none": + return erroredOptions, nil + default: + return erroredOptions, errors.Errorf("Invalid kernel namespace to share: %s. Options are: net, pid, ipc, uts or none", toShare) + } + } + return options, nil +} diff --git a/pkg/specgen/oci.go b/pkg/specgen/generate/oci.go index 0756782b4..4bc4d2327 100644 --- a/pkg/specgen/oci.go +++ b/pkg/specgen/generate/oci.go @@ -1,4 +1,4 @@ -package specgen +package generate import ( "strings" @@ -7,11 +7,12 @@ import ( "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/rootless" createconfig "github.com/containers/libpod/pkg/spec" + "github.com/containers/libpod/pkg/specgen" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-tools/generate" ) -func (s *SpecGenerator) ToOCISpec(rt *libpod.Runtime, newImage *image.Image) (*spec.Spec, error) { +func SpecGenToOCI(s *specgen.SpecGenerator, rt *libpod.Runtime, newImage *image.Image) (*spec.Spec, error) { var ( inUserNS bool ) @@ -183,32 +184,32 @@ func (s *SpecGenerator) ToOCISpec(rt *libpod.Runtime, newImage *image.Image) (*s // NAMESPACES - if err := s.pidConfigureGenerator(&g); err != nil { + if err := pidConfigureGenerator(s, &g); err != nil { return nil, err } - if err := s.userConfigureGenerator(&g); err != nil { + if err := userConfigureGenerator(s, &g); err != nil { return nil, err } - if err := s.networkConfigureGenerator(&g); err != nil { + if err := networkConfigureGenerator(s, &g); err != nil { return nil, err } - if err := s.utsConfigureGenerator(&g, rt); err != nil { + if err := utsConfigureGenerator(s, &g, rt); err != nil { return nil, err } - if err := s.ipcConfigureGenerator(&g); err != nil { + if err := ipcConfigureGenerator(s, &g); err != nil { return nil, err } - if err := s.cgroupConfigureGenerator(&g); err != nil { + if err := cgroupConfigureGenerator(s, &g); err != nil { return nil, err } configSpec := g.Config - if err := s.securityConfigureGenerator(&g, newImage); err != nil { + if err := securityConfigureGenerator(s, &g, newImage); err != nil { return nil, err } diff --git a/pkg/specgen/pod_create.go b/pkg/specgen/generate/pod_create.go index 06aa24e22..292f9b155 100644 --- a/pkg/specgen/pod_create.go +++ b/pkg/specgen/generate/pod_create.go @@ -1,31 +1,31 @@ -package specgen +package generate import ( "context" - "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/specgen" "github.com/sirupsen/logrus" ) -func (p *PodSpecGenerator) MakePod(rt *libpod.Runtime) (*libpod.Pod, error) { - if err := p.validate(); err != nil { +func MakePod(p *specgen.PodSpecGenerator, rt *libpod.Runtime) (*libpod.Pod, error) { + if err := p.Validate(); err != nil { return nil, err } - options, err := p.createPodOptions() + options, err := createPodOptions(p) if err != nil { return nil, err } return rt.NewPod(context.Background(), options...) } -func (p *PodSpecGenerator) createPodOptions() ([]libpod.PodCreateOption, error) { +func createPodOptions(p *specgen.PodSpecGenerator) ([]libpod.PodCreateOption, error) { var ( options []libpod.PodCreateOption ) if !p.NoInfra { options = append(options, libpod.WithInfraContainer()) - nsOptions, err := shared.GetNamespaceOptions(p.SharedNamespaces) + nsOptions, err := GetNamespaceOptions(p.SharedNamespaces) if err != nil { return nil, err } @@ -62,9 +62,9 @@ func (p *PodSpecGenerator) createPodOptions() ([]libpod.PodCreateOption, error) options = append(options, libpod.WithPodUseImageResolvConf()) } switch p.NetNS.NSMode { - case Bridge: + case specgen.Bridge: logrus.Debugf("Pod using default network mode") - case Host: + case specgen.Host: logrus.Debugf("Pod will use host networking") options = append(options, libpod.WithPodHostNetwork()) default: diff --git a/pkg/specgen/storage.go b/pkg/specgen/generate/storage.go index 1b903f608..c9a36ed46 100644 --- a/pkg/specgen/storage.go +++ b/pkg/specgen/generate/storage.go @@ -1,4 +1,4 @@ -package specgen +package generate //nolint @@ -8,9 +8,9 @@ import ( "path/filepath" "strings" - "github.com/containers/libpod/libpod" - "github.com/containers/buildah/pkg/parse" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/specgen" "github.com/containers/libpod/pkg/util" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" @@ -38,7 +38,7 @@ var ( // TODO: Named volume options - should we default to rprivate? It bakes into a // bind mount under the hood... // TODO: handle options parsing/processing via containers/storage/pkg/mount -func (s *SpecGenerator) parseVolumes(mounts, volMounts, tmpMounts []string) error { //nolint +func parseVolumes(s *specgen.SpecGenerator, mounts, volMounts, tmpMounts []string) error { //nolint // TODO this needs to come from the image and erquires a runtime diff --git a/pkg/specgen/namespaces.go b/pkg/specgen/namespaces.go index 2a7bb3495..2ef5bc229 100644 --- a/pkg/specgen/namespaces.go +++ b/pkg/specgen/namespaces.go @@ -1,16 +1,7 @@ package specgen import ( - "os" - - "github.com/containers/common/pkg/capabilities" - "github.com/containers/libpod/libpod" - "github.com/containers/libpod/libpod/image" - "github.com/cri-o/ocicni/pkg/ocicni" - spec "github.com/opencontainers/runtime-spec/specs-go" - "github.com/opencontainers/runtime-tools/generate" "github.com/pkg/errors" - "github.com/sirupsen/logrus" ) type NamespaceMode string @@ -105,373 +96,3 @@ func (n *Namespace) validate() error { } return nil } - -func (s *SpecGenerator) GenerateNamespaceContainerOpts(rt *libpod.Runtime) ([]libpod.CtrCreateOption, error) { - var portBindings []ocicni.PortMapping - options := make([]libpod.CtrCreateOption, 0) - - // Cgroups - switch { - case s.CgroupNS.IsPrivate(): - ns := s.CgroupNS.Value - if _, err := os.Stat(ns); err != nil { - return nil, err - } - case s.CgroupNS.IsContainer(): - connectedCtr, err := rt.LookupContainer(s.CgroupNS.Value) - if err != nil { - return nil, errors.Wrapf(err, "container %q not found", s.CgroupNS.Value) - } - options = append(options, libpod.WithCgroupNSFrom(connectedCtr)) - // TODO - //default: - // return nil, errors.New("cgroup name only supports private and container") - } - - if s.CgroupParent != "" { - options = append(options, libpod.WithCgroupParent(s.CgroupParent)) - } - - if s.CgroupsMode != "" { - options = append(options, libpod.WithCgroupsMode(s.CgroupsMode)) - } - - // ipc - switch { - case s.IpcNS.IsHost(): - options = append(options, libpod.WithShmDir("/dev/shm")) - case s.IpcNS.IsContainer(): - connectedCtr, err := rt.LookupContainer(s.IpcNS.Value) - if err != nil { - return nil, errors.Wrapf(err, "container %q not found", s.IpcNS.Value) - } - options = append(options, libpod.WithIPCNSFrom(connectedCtr)) - options = append(options, libpod.WithShmDir(connectedCtr.ShmDir())) - } - - // pid - if s.PidNS.IsContainer() { - connectedCtr, err := rt.LookupContainer(s.PidNS.Value) - if err != nil { - return nil, errors.Wrapf(err, "container %q not found", s.PidNS.Value) - } - options = append(options, libpod.WithPIDNSFrom(connectedCtr)) - } - - // uts - switch { - case s.UtsNS.IsPod(): - connectedPod, err := rt.LookupPod(s.UtsNS.Value) - if err != nil { - return nil, errors.Wrapf(err, "pod %q not found", s.UtsNS.Value) - } - options = append(options, libpod.WithUTSNSFromPod(connectedPod)) - case s.UtsNS.IsContainer(): - connectedCtr, err := rt.LookupContainer(s.UtsNS.Value) - if err != nil { - return nil, errors.Wrapf(err, "container %q not found", s.UtsNS.Value) - } - - options = append(options, libpod.WithUTSNSFrom(connectedCtr)) - } - - if s.UseImageHosts { - options = append(options, libpod.WithUseImageHosts()) - } else if len(s.HostAdd) > 0 { - options = append(options, libpod.WithHosts(s.HostAdd)) - } - - // User - - switch { - case s.UserNS.IsPath(): - ns := s.UserNS.Value - if ns == "" { - return nil, errors.Errorf("invalid empty user-defined user namespace") - } - _, err := os.Stat(ns) - if err != nil { - return nil, err - } - if s.IDMappings != nil { - options = append(options, libpod.WithIDMappings(*s.IDMappings)) - } - case s.UserNS.IsContainer(): - connectedCtr, err := rt.LookupContainer(s.UserNS.Value) - if err != nil { - return nil, errors.Wrapf(err, "container %q not found", s.UserNS.Value) - } - options = append(options, libpod.WithUserNSFrom(connectedCtr)) - default: - if s.IDMappings != nil { - options = append(options, libpod.WithIDMappings(*s.IDMappings)) - } - } - - options = append(options, libpod.WithUser(s.User)) - options = append(options, libpod.WithGroups(s.Groups)) - - if len(s.PortMappings) > 0 { - portBindings = s.PortMappings - } - - switch { - case s.NetNS.IsPath(): - ns := s.NetNS.Value - if ns == "" { - return nil, errors.Errorf("invalid empty user-defined network namespace") - } - _, err := os.Stat(ns) - if err != nil { - return nil, err - } - case s.NetNS.IsContainer(): - connectedCtr, err := rt.LookupContainer(s.NetNS.Value) - if err != nil { - return nil, errors.Wrapf(err, "container %q not found", s.NetNS.Value) - } - options = append(options, libpod.WithNetNSFrom(connectedCtr)) - case !s.NetNS.IsHost() && s.NetNS.NSMode != NoNetwork: - postConfigureNetNS := !s.UserNS.IsHost() - options = append(options, libpod.WithNetNS(portBindings, postConfigureNetNS, string(s.NetNS.NSMode), s.CNINetworks)) - } - - if len(s.DNSSearch) > 0 { - options = append(options, libpod.WithDNSSearch(s.DNSSearch)) - } - if len(s.DNSServer) > 0 { - // TODO I'm not sure how we are going to handle this given the input - if len(s.DNSServer) == 1 { //&& strings.ToLower(s.DNSServer[0].) == "none" { - options = append(options, libpod.WithUseImageResolvConf()) - } else { - var dnsServers []string - for _, d := range s.DNSServer { - dnsServers = append(dnsServers, d.String()) - } - options = append(options, libpod.WithDNS(dnsServers)) - } - } - if len(s.DNSOption) > 0 { - options = append(options, libpod.WithDNSOption(s.DNSOption)) - } - if s.StaticIP != nil { - options = append(options, libpod.WithStaticIP(*s.StaticIP)) - } - - if s.StaticMAC != nil { - options = append(options, libpod.WithStaticMAC(*s.StaticMAC)) - } - return options, nil -} - -func (s *SpecGenerator) pidConfigureGenerator(g *generate.Generator) error { - if s.PidNS.IsPath() { - return g.AddOrReplaceLinuxNamespace(string(spec.PIDNamespace), s.PidNS.Value) - } - if s.PidNS.IsHost() { - return g.RemoveLinuxNamespace(string(spec.PIDNamespace)) - } - if s.PidNS.IsContainer() { - logrus.Debugf("using container %s pidmode", s.PidNS.Value) - } - if s.PidNS.IsPod() { - logrus.Debug("using pod pidmode") - } - return nil -} - -func (s *SpecGenerator) utsConfigureGenerator(g *generate.Generator, runtime *libpod.Runtime) error { - hostname := s.Hostname - var err error - if hostname == "" { - switch { - case s.UtsNS.IsContainer(): - utsCtr, err := runtime.LookupContainer(s.UtsNS.Value) - if err != nil { - return errors.Wrapf(err, "unable to retrieve hostname from dependency container %s", s.UtsNS.Value) - } - hostname = utsCtr.Hostname() - case s.NetNS.IsHost() || s.UtsNS.IsHost(): - hostname, err = os.Hostname() - if err != nil { - return errors.Wrap(err, "unable to retrieve hostname of the host") - } - default: - logrus.Debug("No hostname set; container's hostname will default to runtime default") - } - } - g.RemoveHostname() - if s.Hostname != "" || !s.UtsNS.IsHost() { - // Set the hostname in the OCI configuration only - // if specified by the user or if we are creating - // a new UTS namespace. - g.SetHostname(hostname) - } - g.AddProcessEnv("HOSTNAME", hostname) - - if s.UtsNS.IsPath() { - return g.AddOrReplaceLinuxNamespace(string(spec.UTSNamespace), s.UtsNS.Value) - } - if s.UtsNS.IsHost() { - return g.RemoveLinuxNamespace(string(spec.UTSNamespace)) - } - if s.UtsNS.IsContainer() { - logrus.Debugf("using container %s utsmode", s.UtsNS.Value) - } - return nil -} - -func (s *SpecGenerator) ipcConfigureGenerator(g *generate.Generator) error { - if s.IpcNS.IsPath() { - return g.AddOrReplaceLinuxNamespace(string(spec.IPCNamespace), s.IpcNS.Value) - } - if s.IpcNS.IsHost() { - return g.RemoveLinuxNamespace(s.IpcNS.Value) - } - if s.IpcNS.IsContainer() { - logrus.Debugf("Using container %s ipcmode", s.IpcNS.Value) - } - return nil -} - -func (s *SpecGenerator) cgroupConfigureGenerator(g *generate.Generator) error { - if s.CgroupNS.IsPath() { - return g.AddOrReplaceLinuxNamespace(string(spec.CgroupNamespace), s.CgroupNS.Value) - } - if s.CgroupNS.IsHost() { - return g.RemoveLinuxNamespace(s.CgroupNS.Value) - } - if s.CgroupNS.IsPrivate() { - return g.AddOrReplaceLinuxNamespace(string(spec.CgroupNamespace), "") - } - if s.CgroupNS.IsContainer() { - logrus.Debugf("Using container %s cgroup mode", s.CgroupNS.Value) - } - return nil -} - -func (s *SpecGenerator) networkConfigureGenerator(g *generate.Generator) error { - switch { - case s.NetNS.IsHost(): - logrus.Debug("Using host netmode") - if err := g.RemoveLinuxNamespace(string(spec.NetworkNamespace)); err != nil { - return err - } - - case s.NetNS.NSMode == NoNetwork: - logrus.Debug("Using none netmode") - case s.NetNS.NSMode == Bridge: - logrus.Debug("Using bridge netmode") - case s.NetNS.IsContainer(): - logrus.Debugf("using container %s netmode", s.NetNS.Value) - case s.NetNS.IsPath(): - logrus.Debug("Using ns netmode") - if err := g.AddOrReplaceLinuxNamespace(string(spec.NetworkNamespace), s.NetNS.Value); err != nil { - return err - } - case s.NetNS.IsPod(): - logrus.Debug("Using pod netmode, unless pod is not sharing") - case s.NetNS.NSMode == Slirp: - logrus.Debug("Using slirp4netns netmode") - default: - return errors.Errorf("unknown network mode") - } - - if g.Config.Annotations == nil { - g.Config.Annotations = make(map[string]string) - } - - if s.PublishImagePorts { - g.Config.Annotations[libpod.InspectAnnotationPublishAll] = libpod.InspectResponseTrue - } else { - g.Config.Annotations[libpod.InspectAnnotationPublishAll] = libpod.InspectResponseFalse - } - - return nil -} - -func (s *SpecGenerator) userConfigureGenerator(g *generate.Generator) error { - if s.UserNS.IsPath() { - if err := g.AddOrReplaceLinuxNamespace(string(spec.UserNamespace), s.UserNS.Value); err != nil { - return err - } - // runc complains if no mapping is specified, even if we join another ns. So provide a dummy mapping - g.AddLinuxUIDMapping(uint32(0), uint32(0), uint32(1)) - g.AddLinuxGIDMapping(uint32(0), uint32(0), uint32(1)) - } - - if s.IDMappings != nil { - if (len(s.IDMappings.UIDMap) > 0 || len(s.IDMappings.GIDMap) > 0) && !s.UserNS.IsHost() { - if err := g.AddOrReplaceLinuxNamespace(string(spec.UserNamespace), ""); err != nil { - return err - } - } - for _, uidmap := range s.IDMappings.UIDMap { - g.AddLinuxUIDMapping(uint32(uidmap.HostID), uint32(uidmap.ContainerID), uint32(uidmap.Size)) - } - for _, gidmap := range s.IDMappings.GIDMap { - g.AddLinuxGIDMapping(uint32(gidmap.HostID), uint32(gidmap.ContainerID), uint32(gidmap.Size)) - } - } - return nil -} - -func (s *SpecGenerator) securityConfigureGenerator(g *generate.Generator, newImage *image.Image) error { - // HANDLE CAPABILITIES - // NOTE: Must happen before SECCOMP - if s.Privileged { - g.SetupPrivileged(true) - } - - useNotRoot := func(user string) bool { - if user == "" || user == "root" || user == "0" { - return false - } - return true - } - configSpec := g.Config - var err error - var caplist []string - bounding := configSpec.Process.Capabilities.Bounding - if useNotRoot(s.User) { - configSpec.Process.Capabilities.Bounding = caplist - } - caplist, err = capabilities.MergeCapabilities(configSpec.Process.Capabilities.Bounding, s.CapAdd, s.CapDrop) - if err != nil { - return err - } - - configSpec.Process.Capabilities.Bounding = caplist - configSpec.Process.Capabilities.Permitted = caplist - configSpec.Process.Capabilities.Inheritable = caplist - configSpec.Process.Capabilities.Effective = caplist - configSpec.Process.Capabilities.Ambient = caplist - if useNotRoot(s.User) { - caplist, err = capabilities.MergeCapabilities(bounding, s.CapAdd, s.CapDrop) - if err != nil { - return err - } - } - configSpec.Process.Capabilities.Bounding = caplist - - // HANDLE SECCOMP - if s.SeccompProfilePath != "unconfined" { - seccompConfig, err := s.getSeccompConfig(configSpec, newImage) - if err != nil { - return err - } - configSpec.Linux.Seccomp = seccompConfig - } - - // Clear default Seccomp profile from Generator for privileged containers - if s.SeccompProfilePath == "unconfined" || s.Privileged { - configSpec.Linux.Seccomp = nil - } - - g.SetRootReadonly(s.ReadOnlyFilesystem) - for sysctlKey, sysctlVal := range s.Sysctl { - g.AddLinuxSysctl(sysctlKey, sysctlVal) - } - - return nil -} diff --git a/pkg/specgen/pod_validate.go b/pkg/specgen/pod_validate.go index 50309f096..9e9659fa9 100644 --- a/pkg/specgen/pod_validate.go +++ b/pkg/specgen/pod_validate.go @@ -15,7 +15,8 @@ func exclusivePodOptions(opt1, opt2 string) error { return errors.Wrapf(ErrInvalidPodSpecConfig, "%s and %s are mutually exclusive pod options", opt1, opt2) } -func (p *PodSpecGenerator) validate() error { +// Validate verifies the input is valid +func (p *PodSpecGenerator) Validate() error { // PodBasicConfig if p.NoInfra { if len(p.InfraCommand) > 0 { @@ -25,7 +26,7 @@ func (p *PodSpecGenerator) validate() error { return exclusivePodOptions("NoInfra", "InfraImage") } if len(p.SharedNamespaces) > 0 { - return exclusivePodOptions("NoInfo", "SharedNamespaces") + return exclusivePodOptions("NoInfra", "SharedNamespaces") } } diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index 2e6dd9c1d..8482ef2c9 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -4,8 +4,6 @@ import ( "net" "syscall" - "github.com/containers/libpod/libpod" - "github.com/containers/image/v5/manifest" "github.com/containers/libpod/pkg/rootless" "github.com/containers/storage" @@ -174,7 +172,7 @@ type ContainerStorageConfig struct { // These will supersede Image Volumes and VolumesFrom volumes where // there are conflicts. // Optional. - Volumes []*libpod.ContainerNamedVolume `json:"volumes,omitempty"` + Volumes []*Volumes `json:"volumes,omitempty"` // Devices are devices that will be added to the container. // Optional. Devices []spec.LinuxDevice `json:"devices,omitempty"` @@ -403,6 +401,13 @@ type SpecGenerator struct { ContainerHealthCheckConfig } +// Volumes is a temporary struct to hold input from the User +type Volumes struct { + Name string + Dest string + Options []string +} + // NewSpecGenerator returns a SpecGenerator struct given one of two mandatory inputs func NewSpecGenerator(image string) *SpecGenerator { networkConfig := ContainerNetworkConfig{ diff --git a/pkg/util/utils.go b/pkg/util/utils.go index 372c7c53b..1051ed311 100644 --- a/pkg/util/utils.go +++ b/pkg/util/utils.go @@ -516,6 +516,8 @@ func ParseInputTime(inputTime string) (time.Time, error) { } // GetGlobalOpts checks all global flags and generates the command string +// FIXME: Port input to config.Config +// TODO: Is there a "better" way to reverse values to flags? This seems brittle. func GetGlobalOpts(c *cliconfig.RunlabelValues) string { globalFlags := map[string]bool{ "cgroup-manager": true, "cni-config-dir": true, "conmon": true, "default-mounts-file": true, diff --git a/test/apiv2/20-containers.at b/test/apiv2/20-containers.at index 04e2fa64c..7fb39b221 100644 --- a/test/apiv2/20-containers.at +++ b/test/apiv2/20-containers.at @@ -21,8 +21,8 @@ t GET libpod/containers/json?all=true 200 \ length=1 \ .[0].Id~[0-9a-f]\\{12\\} \ .[0].Image=$IMAGE \ - .[0].Cmd[0]="true" \ - .[0].ContainerState~\\\(exited\\\|stopped\\\) \ + .[0].Command[0]="true" \ + .[0].State~\\\(exited\\\|stopped\\\) \ .[0].ExitCode=0 \ .[0].IsInfra=false diff --git a/test/apiv2/test-apiv2 b/test/apiv2/test-apiv2 index b101be012..1af76b4be 100755 --- a/test/apiv2/test-apiv2 +++ b/test/apiv2/test-apiv2 @@ -355,7 +355,7 @@ done if [ -n "$service_pid" ]; then kill $service_pid - wait -f $service_pid + wait $service_pid fi test_count=$(<$testcounter_file) diff --git a/test/system/120-load.bats b/test/system/120-load.bats index f2dedb73f..15df6adec 100644 --- a/test/system/120-load.bats +++ b/test/system/120-load.bats @@ -10,7 +10,7 @@ load helpers # # initialize, read image ID and name get_iid_and_name() { - run_podman images --format '{{.ID}} {{.Repository}}:{{.Tag}}' + run_podman images -a --format '{{.ID}} {{.Repository}}:{{.Tag}}' read iid img_name < <(echo "$output") archive=$PODMAN_TMPDIR/myimage-$(random_string 8).tar @@ -18,7 +18,7 @@ get_iid_and_name() { # Simple verification of image ID and name verify_iid_and_name() { - run_podman images --format '{{.ID}} {{.Repository}}:{{.Tag}}' + run_podman images -a --format '{{.ID}} {{.Repository}}:{{.Tag}}' read new_iid new_img_name < <(echo "$output") # Verify diff --git a/version/version.go b/version/version.go index 0d0b0b422..fe602d8e1 100644 --- a/version/version.go +++ b/version/version.go @@ -4,7 +4,7 @@ package version // NOTE: remember to bump the version at the top // of the top-level README.md file when this is // bumped. -const Version = "1.8.3-dev" +const Version = "2.0.0-dev" // RemoteAPIVersion is the version for the remote // client API. It is used to determine compatibility |