diff options
93 files changed, 2456 insertions, 1014 deletions
@@ -263,7 +263,7 @@ localunit: test/goecho/goecho varlink_generate ginkgo \ -r \ $(TESTFLAGS) \ - --skipPackage test/e2e,pkg/apparmor,test/endpoint \ + --skipPackage test/e2e,pkg/apparmor,test/endpoint,pkg/bindings \ --cover \ --covermode atomic \ --tags "$(BUILDTAGS)" \ @@ -452,6 +452,8 @@ install.docker: docker-docs install ${SELINUXOPT} -d -m 755 $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1 install ${SELINUXOPT} -m 755 docker $(DESTDIR)$(BINDIR)/docker install ${SELINUXOPT} -m 644 docs/build/man/docker*.1 -t $(DESTDIR)$(MANDIR)/man1 + install ${SELINUXOPT} -m 755 -d ${DESTDIR}${SYSTEMDDIR} ${DESTDIR}${USERSYSTEMDDIR} ${DESTDIR}${TMPFILESDIR} + install ${SELINUXOPT} -m 644 contrib/systemd/system/podman-docker.conf -t ${DESTDIR}${TMPFILESDIR} install.systemd: install ${SELINUXOPT} -m 755 -d ${DESTDIR}${SYSTEMDDIR} ${DESTDIR}${USERSYSTEMDDIR} ${DESTDIR}${TMPFILESDIR} diff --git a/cmd/podman/common.go b/cmd/podman/common.go index f3aff1d49..46feae90d 100644 --- a/cmd/podman/common.go +++ b/cmd/podman/common.go @@ -12,7 +12,7 @@ import ( "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/rootless" "github.com/containers/libpod/pkg/sysinfo" - "github.com/fatih/camelcase" + "github.com/containers/libpod/pkg/util/camelcase" jsoniter "github.com/json-iterator/go" "github.com/pkg/errors" "github.com/spf13/cobra" diff --git a/cmd/podman/push.go b/cmd/podman/push.go index 1be8dfe11..b078959ba 100644 --- a/cmd/podman/push.go +++ b/cmd/podman/push.go @@ -100,7 +100,8 @@ func pushCmd(c *cliconfig.PushValues) error { // --compress and --format can only be used for the "dir" transport splitArg := strings.SplitN(destName, ":", 2) - if c.Flag("compress").Changed || c.Flag("format").Changed { + + if c.IsSet("compress") || c.Flag("format").Changed { if splitArg[0] != directory.Transport.Name() { return errors.Errorf("--compress and --format can be set only when pushing to a directory using the 'dir' transport") } @@ -141,7 +142,7 @@ func pushCmd(c *cliconfig.PushValues) error { DockerRegistryCreds: registryCreds, DockerCertPath: certPath, } - if c.Flag("tls-verify").Changed { + if c.IsSet("tls-verify") { dockerRegistryOptions.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!c.TlsVerify) } diff --git a/cmd/podman/shared/pod.go b/cmd/podman/shared/pod.go index d8d69c8fc..7b0b497fc 100644 --- a/cmd/podman/shared/pod.go +++ b/cmd/podman/shared/pod.go @@ -10,14 +10,8 @@ import ( "github.com/pkg/errors" ) -const ( - PodStateStopped = "Stopped" - PodStateRunning = "Running" - PodStatePaused = "Paused" - PodStateExited = "Exited" - PodStateErrored = "Error" - PodStateCreated = "Created" -) +// TODO GetPodStatus and CreatePodStatusResults should removed once the adapter +// and shared packages are reworked. It has now been duplicated in libpod proper. // GetPodStatus determines the status of the pod based on the // statuses of the containers in the pod. @@ -25,7 +19,7 @@ const ( func GetPodStatus(pod *libpod.Pod) (string, error) { ctrStatuses, err := pod.Status() if err != nil { - return PodStateErrored, err + return define.PodStateErrored, err } return CreatePodStatusResults(ctrStatuses) } @@ -33,45 +27,45 @@ func GetPodStatus(pod *libpod.Pod) (string, error) { func CreatePodStatusResults(ctrStatuses map[string]define.ContainerStatus) (string, error) { ctrNum := len(ctrStatuses) if ctrNum == 0 { - return PodStateCreated, nil + return define.PodStateCreated, nil } statuses := map[string]int{ - PodStateStopped: 0, - PodStateRunning: 0, - PodStatePaused: 0, - PodStateCreated: 0, - PodStateErrored: 0, + define.PodStateStopped: 0, + define.PodStateRunning: 0, + define.PodStatePaused: 0, + define.PodStateCreated: 0, + define.PodStateErrored: 0, } for _, ctrStatus := range ctrStatuses { switch ctrStatus { case define.ContainerStateExited: fallthrough case define.ContainerStateStopped: - statuses[PodStateStopped]++ + statuses[define.PodStateStopped]++ case define.ContainerStateRunning: - statuses[PodStateRunning]++ + statuses[define.PodStateRunning]++ case define.ContainerStatePaused: - statuses[PodStatePaused]++ + statuses[define.PodStatePaused]++ case define.ContainerStateCreated, define.ContainerStateConfigured: - statuses[PodStateCreated]++ + statuses[define.PodStateCreated]++ default: - statuses[PodStateErrored]++ + statuses[define.PodStateErrored]++ } } switch { - case statuses[PodStateRunning] > 0: - return PodStateRunning, nil - case statuses[PodStatePaused] == ctrNum: - return PodStatePaused, nil - case statuses[PodStateStopped] == ctrNum: - return PodStateExited, nil - case statuses[PodStateStopped] > 0: - return PodStateStopped, nil - case statuses[PodStateErrored] > 0: - return PodStateErrored, nil + case statuses[define.PodStateRunning] > 0: + return define.PodStateRunning, nil + case statuses[define.PodStatePaused] == ctrNum: + return define.PodStatePaused, nil + case statuses[define.PodStateStopped] == ctrNum: + return define.PodStateExited, nil + case statuses[define.PodStateStopped] > 0: + return define.PodStateStopped, nil + case statuses[define.PodStateErrored] > 0: + return define.PodStateErrored, nil default: - return PodStateCreated, nil + return define.PodStateCreated, nil } } diff --git a/contrib/systemd/system/podman-docker.conf b/contrib/systemd/system/podman-docker.conf new file mode 100644 index 000000000..e12f19bce --- /dev/null +++ b/contrib/systemd/system/podman-docker.conf @@ -0,0 +1 @@ +L+ /run/docker.sock - - - - /run/podman/podman.sock diff --git a/docs/source/markdown/podman-container-exists.1.md b/docs/source/markdown/podman-container-exists.1.md index 4d988132b..3b4ca33e4 100644 --- a/docs/source/markdown/podman-container-exists.1.md +++ b/docs/source/markdown/podman-container-exists.1.md @@ -21,7 +21,7 @@ Print usage statement Check if an container called `webclient` exists in local storage (the container does actually exist). ``` -$ sudo podman container exists webclient +$ podman container exists webclient $ echo $? 0 $ @@ -29,7 +29,7 @@ $ Check if an container called `webbackend` exists in local storage (the container does not actually exist). ``` -$ sudo podman container exists webbackend +$ podman container exists webbackend $ echo $? 1 $ diff --git a/docs/source/markdown/podman-exec.1.md b/docs/source/markdown/podman-exec.1.md index 8c0106d70..1bd10f9ba 100644 --- a/docs/source/markdown/podman-exec.1.md +++ b/docs/source/markdown/podman-exec.1.md @@ -80,28 +80,28 @@ when creating the container. The exit code from `podman exec` gives information about why the command within the container failed to run or why it exited. When `podman exec` exits with a non-zero code, the exit codes follow the `chroot` standard, see below: -**_125_** if the error is with Podman **_itself_** + **125** The error is with Podman itself $ podman exec --foo ctrID /bin/sh; echo $? Error: unknown flag: --foo 125 -**_126_** if the **_contained command_** cannot be invoked + **126** The _contained command_ cannot be invoked $ podman exec ctrID /etc; echo $? Error: container_linux.go:346: starting container process caused "exec: \"/etc\": permission denied": OCI runtime error 126 -**_127_** if the **_contained command_** cannot be found + **127** The _contained command_ cannot be found $ podman exec ctrID foo; echo $? Error: container_linux.go:346: starting container process caused "exec: \"foo\": executable file not found in $PATH": OCI runtime error 127 -**_Exit code_** of **_contained command_** otherwise + **Exit code** The _contained command_ exit code - $ podman exec ctrID /bin/sh -c 'exit 3' - # 3 + $ podman exec ctrID /bin/sh -c 'exit 3'; echo $? + 3 ## EXAMPLES diff --git a/docs/source/markdown/podman-history.1.md b/docs/source/markdown/podman-history.1.md index 078864faa..1a8f8906c 100644 --- a/docs/source/markdown/podman-history.1.md +++ b/docs/source/markdown/podman-history.1.md @@ -29,17 +29,17 @@ Valid placeholders for the Go template are listed below: ## OPTIONS -**--human**, **-H** +**--human**, **-H**=*true|false* -Display sizes and dates in human readable format +Display sizes and dates in human readable format (default *true*). -**--no-trunc** +**--no-trunc**=*true|false* -Do not truncate the output +Do not truncate the output (default *false*). -**--quiet**, **-q** +**--quiet**, **-q**=*true|false* -Print the numeric IDs only +Print the numeric IDs only (default *false*). **--format**=*format* diff --git a/docs/source/markdown/podman-image-exists.1.md b/docs/source/markdown/podman-image-exists.1.md index f6a89e2aa..3b7127b64 100644 --- a/docs/source/markdown/podman-image-exists.1.md +++ b/docs/source/markdown/podman-image-exists.1.md @@ -22,7 +22,7 @@ Print usage statement Check if an image called `webclient` exists in local storage (the image does actually exist). ``` -$ sudo podman image exists webclient +$ podman image exists webclient $ echo $? 0 $ @@ -30,7 +30,7 @@ $ Check if an image called `webbackend` exists in local storage (the image does not actually exist). ``` -$ sudo podman image exists webbackend +$ podman image exists webbackend $ echo $? 1 $ diff --git a/docs/source/markdown/podman-inspect.1.md b/docs/source/markdown/podman-inspect.1.md index ad4d0659e..4998772c3 100644 --- a/docs/source/markdown/podman-inspect.1.md +++ b/docs/source/markdown/podman-inspect.1.md @@ -36,7 +36,7 @@ The latest option is not supported on the remote client or when invoked as *podm **--size**, **-s** -Display the total file size if the type is a container +In addition to normal output, display the total file size if the type is a container. ## EXAMPLE diff --git a/docs/source/markdown/podman-remote.1.md b/docs/source/markdown/podman-remote.1.md index bbc54a2a6..eab2405b2 100644 --- a/docs/source/markdown/podman-remote.1.md +++ b/docs/source/markdown/podman-remote.1.md @@ -65,27 +65,27 @@ The exit code from `podman` gives information about why the container failed to run or why it exited. When `podman` commands exit with a non-zero code, the exit codes follow the `chroot` standard, see below: -**_125_** if the error is with podman **_itself_** + **125** The error is with podman itself $ podman run --foo busybox; echo $? Error: unknown flag: --foo - 125 + 125 -**_126_** if executing a **_contained command_** and the **_command_** cannot be invoked + **126** Executing a _contained command_ and the _command_ cannot be invoked $ podman run busybox /etc; echo $? Error: container_linux.go:346: starting container process caused "exec: \"/etc\": permission denied": OCI runtime error - 126 + 126 -**_127_** if executing a **_contained command_** and the **_command_** cannot be found + **127** Executing a _contained command_ and the _command_ cannot be found $ podman run busybox foo; echo $? Error: container_linux.go:346: starting container process caused "exec: \"foo\": executable file not found in $PATH": OCI runtime error - 127 + 127 -**_Exit code_** of **_contained command_** otherwise + **Exit code** _contained command_ exit code - $ podman run busybox /bin/sh -c 'exit 3' - # 3 + $ podman run busybox /bin/sh -c 'exit 3'; echo $? + 3 ## COMMANDS diff --git a/docs/source/markdown/podman-rm.1.md b/docs/source/markdown/podman-rm.1.md index 782feac6f..cddf06e3e 100644 --- a/docs/source/markdown/podman-rm.1.md +++ b/docs/source/markdown/podman-rm.1.md @@ -10,13 +10,13 @@ podman\-rm - Remove one or more containers ## DESCRIPTION **podman rm** will remove one or more containers from the host. The container name or ID can be used. This does not remove images. -Running or unusable containers will not be removed without the `-f` option. +Running or unusable containers will not be removed without the **-f** option. ## OPTIONS **--all**, **-a** -Remove all containers. Can be used in conjunction with -f as well. +Remove all containers. Can be used in conjunction with **-f** as well. **--cidfile** @@ -46,51 +46,54 @@ The latest option is not supported on the remote client. **--storage** Remove the container from the storage library only. -This is only possible with containers that are not present in libpod (cannot be seen by `podman ps`). -It is used to remove containers from `podman build` and `buildah`, and orphan containers which were only partially removed by `podman rm`. +This is only possible with containers that are not present in libpod (cannot be seen by **podman ps**). +It is used to remove containers from **podman build** and **buildah**, and orphan containers which were only partially removed by **podman rm**. The storage option conflicts with the **--all**, **--latest**, and **--volumes** options. **--volumes**, **-v** Remove anonymous volumes associated with the container. This does not include named volumes -created with `podman volume create`, or the `--volume` option of `podman run` and `podman create`. +created with **podman volume create**, or the **--volume** option of **podman run** and **podman create**. ## EXAMPLE Remove a container by its name *mywebserver* ``` -podman rm mywebserver +$ podman rm mywebserver ``` Remove several containers by name and container id. ``` -podman rm mywebserver myflaskserver 860a4b23 +$ podman rm mywebserver myflaskserver 860a4b23 ``` Remove several containers reading their IDs from files. ``` -podman rm --cidfile ./cidfile-1 --cidfile /home/user/cidfile-2 +$ podman rm --cidfile ./cidfile-1 --cidfile /home/user/cidfile-2 ``` Forcibly remove a container by container ID. ``` -podman rm -f 860a4b23 +$ podman rm -f 860a4b23 ``` Remove all containers regardless of its run state. ``` -podman rm -f -a +$ podman rm -f -a ``` Forcibly remove the latest container created. ``` -podman rm -f --latest +$ podman rm -f --latest ``` ## Exit Status -**_0_** if all specified containers removed -**_1_** if one of the specified containers did not exist, and no other failures -**_2_** if one of the specified containers is paused or running -**_125_** if the command fails for a reason other than container did not exist or is paused/running + **0** All specified containers removed + + **1** One of the specified containers did not exist, and no other failures + + **2** One of the specified containers is paused or running + + **125** The command fails for a reason other than container did not exist or is paused/running ## SEE ALSO podman(1), podman-image-rm(1) diff --git a/docs/source/markdown/podman-rmi.1.md b/docs/source/markdown/podman-rmi.1.md index 3c46bc32c..78ef2b157 100644 --- a/docs/source/markdown/podman-rmi.1.md +++ b/docs/source/markdown/podman-rmi.1.md @@ -24,27 +24,30 @@ This option will cause podman to remove all containers that are using the image Remove an image by its short ID ``` -podman rmi c0ed59d05ff7 +$ podman rmi c0ed59d05ff7 ``` Remove an image and its associated containers. ``` -podman rmi --force imageID +$ podman rmi --force imageID ``` Remove multiple images by their shortened IDs. ``` -podman rmi c4dfb1609ee2 93fd78260bd1 c0ed59d05ff7 +$ podman rmi c4dfb1609ee2 93fd78260bd1 c0ed59d05ff7 ``` Remove all images and containers. ``` -podman rmi -a -f +$ podman rmi -a -f ``` ## Exit Status -**_0_** if all specified images removed -**_1_** if one of the specified images did not exist, and no other failures -**_2_** if one of the specified images has child images or is being used by a container -**_125_** if the command fails for a reason other than an image did not exist or is in use + **0** All specified images removed + + **1** One of the specified images did not exist, and no other failures + + **2** One of the specified images has child images or is being used by a container + + **125** The command fails for a reason other than an image did not exist or is in use ## SEE ALSO podman(1) diff --git a/docs/source/markdown/podman-run.1.md b/docs/source/markdown/podman-run.1.md index 512a382a6..bf79ea031 100644 --- a/docs/source/markdown/podman-run.1.md +++ b/docs/source/markdown/podman-run.1.md @@ -1022,25 +1022,25 @@ The exit code from `podman run` gives information about why the container failed to run or why it exited. When `podman run` exits with a non-zero code, the exit codes follow the `chroot` standard, see below: -**_125_** if the error is with Podman **_itself_** + **125** The error is with Podman itself $ podman run --foo busybox; echo $? Error: unknown flag: --foo 125 -**_126_** if the **_contained command_** cannot be invoked + **126** The _contained command_ cannot be invoked $ podman run busybox /etc; echo $? Error: container_linux.go:346: starting container process caused "exec: \"/etc\": permission denied": OCI runtime error 126 -**_127_** if the **_contained command_** cannot be found + **127** The _contained command_ cannot be found $ podman run busybox foo; echo $? Error: container_linux.go:346: starting container process caused "exec: \"foo\": executable file not found in $PATH": OCI runtime error 127 -**_Exit code_** of **_contained command_** otherwise + **Exit code** _contained command_ exit code $ podman run busybox /bin/sh -c 'exit 3' 3 diff --git a/docs/source/markdown/podman-stop.1.md b/docs/source/markdown/podman-stop.1.md index 3b5f17057..7dbf18887 100644 --- a/docs/source/markdown/podman-stop.1.md +++ b/docs/source/markdown/podman-stop.1.md @@ -38,27 +38,27 @@ to run containers such as CRI-O, the last started container could be from either The latest option is not supported on the remote client. -**--timeout**, **--time**, **t**=*time* +**--timeout**, **--time**, **-t**=*time* Timeout to wait before forcibly stopping the container -## EXAMPLE +## EXAMPLES -podman stop mywebserver +$ podman stop mywebserver -podman stop 860a4b235279 +$ podman stop 860a4b235279 -podman stop mywebserver 860a4b235279 +$ podman stop mywebserver 860a4b235279 -podman stop --cidfile /home/user/cidfile-1 +$ podman stop --cidfile /home/user/cidfile-1 -podman stop --cidfile /home/user/cidfile-1 --cidfile ./cidfile-2 +$ podman stop --cidfile /home/user/cidfile-1 --cidfile ./cidfile-2 -podman stop --timeout 2 860a4b235279 +$ podman stop --timeout 2 860a4b235279 -podman stop -a +$ podman stop -a -podman stop --latest +$ podman stop --latest ## SEE ALSO podman(1), podman-rm(1) diff --git a/docs/source/markdown/podman-system-df.1.md b/docs/source/markdown/podman-system-df.1.md index d0b1755ee..3ddb685fc 100644 --- a/docs/source/markdown/podman-system-df.1.md +++ b/docs/source/markdown/podman-system-df.1.md @@ -18,7 +18,7 @@ Pretty-print images using a Go template Show detailed information on space usage ## EXAMPLE - +``` $ podman system df TYPE TOTAL ACTIVE SIZE RECLAIMABLE Images 6 2 281MB 168MB (59%) @@ -49,7 +49,7 @@ $ podman system df --format "{{.Type}}\t{{.Total}}" Images 1 Containers 5 Local Volumes 1 - +``` ## SEE ALSO podman-system(1) diff --git a/docs/source/markdown/podman-system-prune.1.md b/docs/source/markdown/podman-system-prune.1.md index e6297dc0b..9c6ef5d8b 100644 --- a/docs/source/markdown/podman-system-prune.1.md +++ b/docs/source/markdown/podman-system-prune.1.md @@ -9,9 +9,9 @@ podman\-system\-prune - Remove all unused container, image and volume data ## DESCRIPTION **podman system prune** removes all unused containers (both dangling and unreferenced), pods and optionally, volumes from local storage. -With the `all` option, you can delete all unused images. Unused images are dangling images as well as any image that does not have any containers based on it. +With the **--all** option, you can delete all unused images. Unused images are dangling images as well as any image that does not have any containers based on it. -By default, volumes are not removed to prevent important data from being deleted if there is currently no container using the volume. Use the --volumes flag when running the command to prune volumes as well. +By default, volumes are not removed to prevent important data from being deleted if there is currently no container using the volume. Use the **--volumes** flag when running the command to prune volumes as well. ## OPTIONS **--all**, **-a** @@ -28,7 +28,7 @@ Print usage statement **--volumes** -Prune volumes not used by at least one container +Prune volumes currently unused by any container ## SEE ALSO podman(1), podman-image-prune(1), podman-container-prune(1), podman-pod-prune(1), podman-volume-prune(1) diff --git a/docs/source/markdown/podman.1.md b/docs/source/markdown/podman.1.md index 6e0eff045..acb546ddd 100644 --- a/docs/source/markdown/podman.1.md +++ b/docs/source/markdown/podman.1.md @@ -126,27 +126,27 @@ The exit code from `podman` gives information about why the container failed to run or why it exited. When `podman` commands exit with a non-zero code, the exit codes follow the `chroot` standard, see below: -**_125_** if the error is with podman **_itself_** + **125** The error is with podman **_itself_** $ podman run --foo busybox; echo $? Error: unknown flag: --foo - 125 + 125 -**_126_** if executing a **_contained command_** and the **_command_** cannot be invoked + **126** Executing a _contained command_ and the _command_ cannot be invoked $ podman run busybox /etc; echo $? Error: container_linux.go:346: starting container process caused "exec: \"/etc\": permission denied": OCI runtime error - 126 + 126 -**_127_** if executing a **_contained command_** and the **_command_** cannot be found + **127** Executing a _contained command_ and the _command_ cannot be found $ podman run busybox foo; echo $? Error: container_linux.go:346: starting container process caused "exec: \"foo\": executable file not found in $PATH": OCI runtime error - 127 + 127 -**_Exit code_** of **_contained command_** otherwise + **Exit code** _contained command_ exit code - $ podman run busybox /bin/sh -c 'exit 3' - # 3 + $ podman run busybox /bin/sh -c 'exit 3'; echo $? + 3 ## COMMANDS diff --git a/docs/source/network.rst b/docs/source/network.rst index d96e00a7d..e7848c90e 100644 --- a/docs/source/network.rst +++ b/docs/source/network.rst @@ -1,5 +1,5 @@ Network -===== +======= :doc:`create <markdown/podman-network-create.1>` network create @@ -29,7 +29,7 @@ require ( github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect github.com/elazarl/goproxy/ext v0.0.0-20190911111923-ecfe977594f1 // indirect github.com/etcd-io/bbolt v1.3.3 - github.com/fatih/camelcase v1.0.0 + github.com/fatih/camelcase v1.0.0 // indirect github.com/fsnotify/fsnotify v1.4.7 github.com/ghodss/yaml v1.0.0 github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e @@ -54,7 +54,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/pkg/profile v1.4.0 // indirect github.com/pmezard/go-difflib v1.0.0 - github.com/rootless-containers/rootlesskit v0.7.1 + github.com/rootless-containers/rootlesskit v0.7.2 github.com/seccomp/containers-golang v0.0.0-20190312124753-8ca8945ccf5f github.com/sirupsen/logrus v1.4.2 github.com/spf13/cobra v0.0.5 @@ -72,7 +72,7 @@ require ( golang.org/x/sys v0.0.0-20191127021746-63cb32ae39b2 google.golang.org/appengine v1.6.1 // indirect google.golang.org/genproto v0.0.0-20190620144150-6af8c5fc6601 // indirect - gopkg.in/yaml.v2 v2.2.7 + gopkg.in/yaml.v2 v2.2.8 k8s.io/api v0.17.2 k8s.io/apimachinery v0.17.2 k8s.io/client-go v0.0.0-20190620085101-78d2af792bab @@ -456,6 +456,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uY github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= github.com/rootless-containers/rootlesskit v0.7.1 h1:enhwHIAXDjfpV83bL4xF60WZ+1ATjMB7spDDvpWAfPk= github.com/rootless-containers/rootlesskit v0.7.1/go.mod h1:r9YL5mKRIdnwcYk4G8E5CSc9MDeFtgYmhfE4CSvDGYA= +github.com/rootless-containers/rootlesskit v0.7.2 h1:gcWQ9/GN98ne1AqnoeOgQ8e6qpKd3BuB4ug+2h95Fr0= +github.com/rootless-containers/rootlesskit v0.7.2/go.mod h1:r9YL5mKRIdnwcYk4G8E5CSc9MDeFtgYmhfE4CSvDGYA= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8 h1:2c1EFnZHIPCW8qKWgHMH/fX2PkSabFc5mrVzfUNdg5U= @@ -698,6 +700,8 @@ gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v0.0.0-20190624233834-05ebafbffc79/go.mod h1:R//lfYlUuTOTfblYI3lGoAAAebUdzjvbmQsuB7Ykd90= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= diff --git a/install.md b/install.md index 90ad4f233..561c4afe9 100644 --- a/install.md +++ b/install.md @@ -35,8 +35,6 @@ wget -nv https://download.opensuse.org/repositories/devel:kubic:libcontainers:st sudo apt-key add - < Release.key sudo apt-get update -qq sudo apt-get -qq -y install podman -sudo mkdir -p /etc/containers -echo -e "[registries.search]\nregistries = ['docker.io', 'quay.io']" | sudo tee /etc/containers/registries.conf ``` There are many [packages](https://packages.debian.org/search?keywords=libpod&searchon=names&suite=stable§ion=all) @@ -68,6 +66,16 @@ Using [Homebrew](https://brew.sh/): brew cask install podman ``` +#### [OpenEmbedded](https://www.openembedded.org) + +Bitbake recipes for podman and its dependencies are available in the +[meta-virtualization layer](https://git.yoctoproject.org/cgit/cgit.cgi/meta-virtualization/). +Add the layer to your OpenEmbedded build environment and build podman using: + +```bash +bitbake podman +``` + #### [openSUSE](https://www.opensuse.org) ```bash @@ -90,8 +98,6 @@ wget -nv https://download.opensuse.org/repositories/devel:kubic:libcontainers:st sudo apt-key add - < Release.key sudo apt-get update -qq sudo apt-get -qq -y install podman -sudo mkdir -p /etc/containers -echo -e "[registries.search]\nregistries = ['docker.io', 'quay.io']" | sudo tee /etc/containers/registries.conf ``` @@ -159,8 +165,6 @@ wget -nv https://download.opensuse.org/repositories/devel:kubic:libcontainers:te sudo apt-key add - < Release.key sudo apt-get update -qq sudo apt-get -qq -y install podman -sudo mkdir -p /etc/containers -echo -e "[registries.search]\nregistries = ['docker.io', 'quay.io']" | sudo tee /etc/containers/registries.conf ``` @@ -177,6 +181,8 @@ If you use a newer Podman package from Fedora's `updates-testing`, we would appreciate your `+1` feedback in [Bodhi, Fedora's update management system](https://bodhi.fedoraproject.org/updates/?packages=podman). +If you are running a non-rawhide Fedora distribution, you can also test the latest packages +with our [COPR repository](https://copr.fedorainfracloud.org/coprs/baude/Upstream_CRIO_Family/). #### [Raspbian](https://raspbian.org) @@ -189,8 +195,6 @@ wget -nv https://download.opensuse.org/repositories/devel:kubic:libcontainers:te sudo apt-key add - < Release.key sudo apt-get update -qq sudo apt-get -qq -y install podman -sudo mkdir -p /etc/containers -echo -e "[registries.search]\nregistries = ['docker.io', 'quay.io']" | sudo tee /etc/containers/registries.conf ``` @@ -205,8 +209,6 @@ wget -nv https://download.opensuse.org/repositories/devel:kubic:libcontainers:te sudo apt-key add - < Release.key sudo apt-get update -qq sudo apt-get -qq -y install podman -sudo mkdir -p /etc/containers -echo -e "[registries.search]\nregistries = ['docker.io', 'quay.io']" | sudo tee /etc/containers/registries.conf ``` diff --git a/libpod/container_inspect.go b/libpod/container_inspect.go index 01f2d93bd..641bc8a91 100644 --- a/libpod/container_inspect.go +++ b/libpod/container_inspect.go @@ -1014,6 +1014,9 @@ func (c *Container) generateInspectContainerHostConfig(ctrSpec *spec.Spec, named hostConfig.ShmSize = c.config.ShmSize hostConfig.Runtime = "oci" + // Default CPUShares is 1024, but we may overwrite below. + hostConfig.CpuShares = 1024 + // This is very expensive to initialize. // So we don't want to initialize it unless we absolutely have to - IE, // there are things that require a major:minor to path translation. diff --git a/libpod/define/podstate.go b/libpod/define/podstate.go new file mode 100644 index 000000000..2b59aabfb --- /dev/null +++ b/libpod/define/podstate.go @@ -0,0 +1,19 @@ +package define + +const ( + // PodStateCreated indicates the pod is created but has not been started + PodStateCreated = "Created" + // PodStateErrored indicates the pod is in an errored state where + // information about it can no longer be retrieved + PodStateErrored = "Error" + // PodStateExited indicates the pod ran but has been stopped + PodStateExited = "Exited" + // PodStatePaused indicates the pod has been paused + PodStatePaused = "Paused" + // PodStateRunning indicates that one or more of the containers in + // the pod is running + PodStateRunning = "Running" + // PodStateStopped indicates all of the containers belonging to the pod + // are stopped. + PodStateStopped = "Stopped" +) diff --git a/libpod/image/image.go b/libpod/image/image.go index 6ea49e2a9..7ee8c45d7 100644 --- a/libpod/image/image.go +++ b/libpod/image/image.go @@ -1012,6 +1012,15 @@ func (i *Image) Inspect(ctx context.Context) (*inspect.ImageData, error) { History: ociv1Img.History, NamesHistory: i.NamesHistory(), } + if manifestType == manifest.DockerV2Schema2MediaType { + hc, err := i.GetHealthCheck(ctx) + if err != nil { + return nil, err + } + if hc != nil { + data.HealthCheck = hc + } + } return data, nil } diff --git a/libpod/pod_status.go b/libpod/pod_status.go new file mode 100644 index 000000000..3a44c4457 --- /dev/null +++ b/libpod/pod_status.go @@ -0,0 +1,59 @@ +package libpod + +import "github.com/containers/libpod/libpod/define" + +// GetPodStatus determines the status of the pod based on the +// statuses of the containers in the pod. +// Returns a string representation of the pod status +func (p *Pod) GetPodStatus() (string, error) { + ctrStatuses, err := p.Status() + if err != nil { + return define.PodStateErrored, err + } + return CreatePodStatusResults(ctrStatuses) +} + +func CreatePodStatusResults(ctrStatuses map[string]define.ContainerStatus) (string, error) { + ctrNum := len(ctrStatuses) + if ctrNum == 0 { + return define.PodStateCreated, nil + } + statuses := map[string]int{ + define.PodStateStopped: 0, + define.PodStateRunning: 0, + define.PodStatePaused: 0, + define.PodStateCreated: 0, + define.PodStateErrored: 0, + } + for _, ctrStatus := range ctrStatuses { + switch ctrStatus { + case define.ContainerStateExited: + fallthrough + case define.ContainerStateStopped: + statuses[define.PodStateStopped]++ + case define.ContainerStateRunning: + statuses[define.PodStateRunning]++ + case define.ContainerStatePaused: + statuses[define.PodStatePaused]++ + case define.ContainerStateCreated, define.ContainerStateConfigured: + statuses[define.PodStateCreated]++ + default: + statuses[define.PodStateErrored]++ + } + } + + switch { + case statuses[define.PodStateRunning] > 0: + return define.PodStateRunning, nil + case statuses[define.PodStatePaused] == ctrNum: + return define.PodStatePaused, nil + case statuses[define.PodStateStopped] == ctrNum: + return define.PodStateExited, nil + case statuses[define.PodStateStopped] > 0: + return define.PodStateStopped, nil + case statuses[define.PodStateErrored] > 0: + return define.PodStateErrored, nil + default: + return define.PodStateCreated, nil + } +} diff --git a/libpod/runtime_pod.go b/libpod/runtime_pod.go index 66f9b10c9..e1dc31391 100644 --- a/libpod/runtime_pod.go +++ b/libpod/runtime_pod.go @@ -182,3 +182,31 @@ func (r *Runtime) GetRunningPods() ([]*Pod, error) { } return runningPods, nil } + +// PrunePods removes unused pods and their containers from local storage. +// If force is given, then running pods are also included in the pruning. +func (r *Runtime) PrunePods() (map[string]error, error) { + response := make(map[string]error) + states := []string{define.PodStateStopped, define.PodStateExited} + filterFunc := func(p *Pod) bool { + state, _ := p.GetPodStatus() + for _, status := range states { + if state == status { + return true + } + } + return false + } + pods, err := r.Pods(filterFunc) + if err != nil { + return nil, err + } + if len(pods) < 1 { + return response, nil + } + for _, pod := range pods { + err := r.removePod(context.TODO(), pod, true, false) + response[pod.ID()] = err + } + return response, nil +} diff --git a/pkg/adapter/pods.go b/pkg/adapter/pods.go index 5891c361f..f89fc7011 100644 --- a/pkg/adapter/pods.go +++ b/pkg/adapter/pods.go @@ -58,9 +58,9 @@ func (r *LocalRuntime) PrunePods(ctx context.Context, cli *cliconfig.PodPruneVal } logrus.Debugf("Setting maximum rm workers to %d", maxWorkers) - states := []string{shared.PodStateStopped, shared.PodStateExited} + states := []string{define.PodStateStopped, define.PodStateExited} if cli.Force { - states = append(states, shared.PodStateRunning) + states = append(states, define.PodStateRunning) } pods, err := r.GetPodsByStatus(states) diff --git a/pkg/adapter/pods_remote.go b/pkg/adapter/pods_remote.go index 16d34769e..5ef1a9216 100644 --- a/pkg/adapter/pods_remote.go +++ b/pkg/adapter/pods_remote.go @@ -540,9 +540,9 @@ func (r *LocalRuntime) PrunePods(ctx context.Context, cli *cliconfig.PodPruneVal ok = []string{} failures = map[string]error{} ) - states := []string{shared.PodStateStopped, shared.PodStateExited} + states := []string{define.PodStateStopped, define.PodStateExited} if cli.Force { - states = append(states, shared.PodStateRunning) + states = append(states, define.PodStateRunning) } ids, err := iopodman.GetPodsByStatus().Call(r.Conn, states) diff --git a/pkg/api/handlers/generic/containers_create.go b/pkg/api/handlers/containers_create.go index edefd5757..4781b23bc 100644 --- a/pkg/api/handlers/generic/containers_create.go +++ b/pkg/api/handlers/containers_create.go @@ -1,4 +1,4 @@ -package generic +package handlers import ( "encoding/json" @@ -10,7 +10,6 @@ import ( "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" image2 "github.com/containers/libpod/libpod/image" - "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/containers/libpod/pkg/namespaces" createconfig "github.com/containers/libpod/pkg/spec" @@ -25,7 +24,7 @@ import ( func CreateContainer(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) decoder := r.Context().Value("decoder").(*schema.Decoder) - input := handlers.CreateContainerConfig{} + input := CreateContainerConfig{} query := struct { Name string `schema:"name"` }{ @@ -74,13 +73,13 @@ func CreateContainer(w http.ResponseWriter, r *http.Request) { } response := ContainerCreateResponse{ - Id: ctr.ID(), + ID: ctr.ID(), Warnings: []string{}} utils.WriteResponse(w, http.StatusCreated, response) } -func makeCreateConfig(input handlers.CreateContainerConfig, newImage *image2.Image) (createconfig.CreateConfig, error) { +func makeCreateConfig(input CreateContainerConfig, newImage *image2.Image) (createconfig.CreateConfig, error) { var ( err error init bool diff --git a/pkg/api/handlers/decoder.go b/pkg/api/handlers/decoder.go new file mode 100644 index 000000000..890d77ecc --- /dev/null +++ b/pkg/api/handlers/decoder.go @@ -0,0 +1,91 @@ +package handlers + +import ( + "encoding/json" + "reflect" + "time" + + "github.com/gorilla/schema" + "github.com/sirupsen/logrus" +) + +// NewAPIDecoder returns a configured schema.Decoder +func NewAPIDecoder() *schema.Decoder { + _ = ParseDateTime + + d := schema.NewDecoder() + d.IgnoreUnknownKeys(true) + d.RegisterConverter(map[string][]string{}, convertUrlValuesString) + d.RegisterConverter(time.Time{}, convertTimeString) + return d +} + +// On client: +// v := map[string][]string{ +// "dangling": {"true"}, +// } +// +// payload, err := jsoniter.MarshalToString(v) +// if err != nil { +// panic(err) +// } +// payload = url.QueryEscape(payload) +func convertUrlValuesString(query string) reflect.Value { + f := map[string][]string{} + + err := json.Unmarshal([]byte(query), &f) + if err != nil { + logrus.Infof("convertUrlValuesString: Failed to Unmarshal %s: %s", query, err.Error()) + } + + return reflect.ValueOf(f) +} + +// isZero() can be used to determine if parsing failed. +func convertTimeString(query string) reflect.Value { + var ( + err error + t time.Time + ) + + for _, f := range []string{ + time.UnixDate, + time.ANSIC, + time.RFC1123, + time.RFC1123Z, + time.RFC3339, + time.RFC3339Nano, + time.RFC822, + time.RFC822Z, + time.RFC850, + time.RubyDate, + time.Stamp, + time.StampMicro, + time.StampMilli, + time.StampNano, + } { + t, err = time.Parse(f, query) + if err == nil { + return reflect.ValueOf(t) + } + + if _, isParseError := err.(*time.ParseError); isParseError { + // Try next format + continue + } else { + break + } + } + + // We've exhausted all formats, or something bad happened + if err != nil { + logrus.Infof("convertTimeString: Failed to parse %s: %s", query, err.Error()) + } + return reflect.ValueOf(time.Time{}) +} + +// ParseDateTime is a helper function to aid in parsing different Time/Date formats +// isZero() can be used to determine if parsing failed. +func ParseDateTime(query string) time.Time { + return convertTimeString(query).Interface().(time.Time) +} diff --git a/pkg/api/handlers/events.go b/pkg/api/handlers/events.go index 900efa3da..44bf35254 100644 --- a/pkg/api/handlers/events.go +++ b/pkg/api/handlers/events.go @@ -1,9 +1,10 @@ package handlers import ( - "encoding/json" "fmt" "net/http" + "strings" + "time" "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/pkg/errors" @@ -11,30 +12,24 @@ import ( func GetEvents(w http.ResponseWriter, r *http.Request) { query := struct { - Since string `json:"since"` - Until string `json:"until"` - Filters string `json:"filters"` + Since time.Time `schema:"since"` + Until time.Time `schema:"until"` + Filters map[string][]string `schema:"filters"` }{} if err := decodeQuery(r, &query); err != nil { utils.Error(w, "Failed to parse parameters", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) } - var filters = map[string][]string{} - if found := hasVar(r, "filters"); found { - if err := json.Unmarshal([]byte(query.Filters), &filters); err != nil { - utils.BadRequest(w, "filters", query.Filters, err) - return + var libpodFilters = []string{} + if _, found := r.URL.Query()["filters"]; found { + for k, v := range query.Filters { + libpodFilters = append(libpodFilters, fmt.Sprintf("%s=%s", k, v[0])) } } - var libpodFilters = make([]string, len(filters)) - for k, v := range filters { - libpodFilters = append(libpodFilters, fmt.Sprintf("%s=%s", k, v[0])) - } - libpodEvents, err := getRuntime(r).GetEvents(libpodFilters) if err != nil { - utils.BadRequest(w, "filters", query.Filters, err) + utils.BadRequest(w, "filters", strings.Join(r.URL.Query()["filters"], ", "), err) return } diff --git a/pkg/api/handlers/generic/config.go b/pkg/api/handlers/generic/config.go deleted file mode 100644 index f715d25eb..000000000 --- a/pkg/api/handlers/generic/config.go +++ /dev/null @@ -1,9 +0,0 @@ -package generic - -// ContainerCreateResponse is the response struct for creating a container -type ContainerCreateResponse struct { - // ID of the container created - Id string `json:"Id"` - // Warnings during container creation - Warnings []string `json:"Warnings"` -} diff --git a/pkg/api/handlers/generic/images.go b/pkg/api/handlers/generic/images.go index 395f64064..ba1cf47b4 100644 --- a/pkg/api/handlers/generic/images.go +++ b/pkg/api/handlers/generic/images.go @@ -6,7 +6,6 @@ import ( "io/ioutil" "net/http" "os" - "strconv" "strings" "github.com/containers/buildah" @@ -16,12 +15,10 @@ import ( "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/containers/libpod/pkg/util" - "github.com/containers/storage" "github.com/docker/docker/api/types" "github.com/gorilla/mux" "github.com/gorilla/schema" "github.com/pkg/errors" - "github.com/sirupsen/logrus" ) func ExportImage(w http.ResponseWriter, r *http.Request) { @@ -59,17 +56,14 @@ func ExportImage(w http.ResponseWriter, r *http.Request) { } func PruneImages(w http.ResponseWriter, r *http.Request) { - // 200 no error - // 500 internal var ( - dangling bool = true - err error + filters []string ) decoder := r.Context().Value("decoder").(*schema.Decoder) runtime := r.Context().Value("runtime").(*libpod.Runtime) query := struct { - filters map[string]string + Filters map[string][]string `schema:"filters"` }{ // This is where you can override the golang default value for one of fields } @@ -79,61 +73,24 @@ func PruneImages(w http.ResponseWriter, r *http.Request) { return } - // FIXME This is likely wrong due to it not being a map[string][]string - - // until ts is not supported on podman prune - if len(query.filters["until"]) > 0 { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "until is not supported yet")) - return - } - // labels are not supported on podman prune - if len(query.filters["label"]) > 0 { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "labelis not supported yet")) - return - } - - if len(query.filters["dangling"]) > 0 { - dangling, err = strconv.ParseBool(query.filters["dangling"]) - if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "processing dangling filter")) - return + idr := []types.ImageDeleteResponseItem{} + for k, v := range query.Filters { + for _, val := range v { + filters = append(filters, fmt.Sprintf("%s=%s", k, val)) } } - idr := []types.ImageDeleteResponseItem{} - // - // This code needs to be migrated to libpod to work correctly. I could not - // work my around the information docker needs with the existing prune in libpod. - // - pruneImages, err := runtime.ImageRuntime().GetPruneImages(!dangling, []image2.ImageFilter{}) + pruneCids, err := runtime.ImageRuntime().PruneImages(r.Context(), false, filters) if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to get images to prune")) + utils.InternalServerError(w, err) return } - for _, p := range pruneImages { - repotags, err := p.RepoTags() - if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to get repotags for image")) - return - } - if err := p.Remove(r.Context(), true); err != nil { - if errors.Cause(err) == storage.ErrImageUsedByContainer { - logrus.Warnf("Failed to prune image %s as it is in use: %v", p.ID(), err) - continue - } - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to prune image")) - return - } - // newimageevent is not export therefore we cannot record the event. this will be fixed - // when the prune is fixed in libpod - // defer p.newImageEvent(events.Prune) - response := types.ImageDeleteResponseItem{ - Deleted: fmt.Sprintf("sha256:%s", p.ID()), // I ack this is not ideal - } - if len(repotags) > 0 { - response.Untagged = repotags[0] - } - idr = append(idr, response) + for _, p := range pruneCids { + idr = append(idr, types.ImageDeleteResponseItem{ + Deleted: p, + }) } + + //FIXME/TODO to do this exacty correct, pruneimages needs to return idrs and space-reclaimed, then we are golden ipr := types.ImagesPruneReport{ ImagesDeleted: idr, SpaceReclaimed: 1, // TODO we cannot supply this right now @@ -343,8 +300,6 @@ func GetImage(w http.ResponseWriter, r *http.Request) { } func GetImages(w http.ResponseWriter, r *http.Request) { - // 200 ok - // 500 internal images, err := utils.GetImages(w, r) if err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Failed get images")) diff --git a/pkg/api/handlers/generic/ping.go b/pkg/api/handlers/generic/ping.go index 44a67d53f..00afd86bc 100644 --- a/pkg/api/handlers/generic/ping.go +++ b/pkg/api/handlers/generic/ping.go @@ -3,6 +3,8 @@ package generic import ( "fmt" "net/http" + + "github.com/containers/libpod/pkg/api/handlers" ) func PingGET(w http.ResponseWriter, _ *http.Request) { @@ -16,7 +18,7 @@ func PingHEAD(w http.ResponseWriter, _ *http.Request) { } func setHeaders(w http.ResponseWriter) { - w.Header().Set("API-Version", DefaultApiVersion) + w.Header().Set("API-Version", handlers.DefaultApiVersion) w.Header().Set("BuildKit-Version", "") w.Header().Set("Docker-Experimental", "true") w.Header().Set("Cache-Control", "no-cache") diff --git a/pkg/api/handlers/generic/swagger.go b/pkg/api/handlers/generic/swagger.go index 27e1fc18d..bfe527c41 100644 --- a/pkg/api/handlers/generic/swagger.go +++ b/pkg/api/handlers/generic/swagger.go @@ -1,11 +1,13 @@ package generic +import "github.com/containers/libpod/pkg/api/handlers" + // Create container // swagger:response ContainerCreateResponse type swagCtrCreateResponse struct { // in:body Body struct { - ContainerCreateResponse + handlers.ContainerCreateResponse } } diff --git a/pkg/api/handlers/handler.go b/pkg/api/handlers/handler.go index 4f303f6ab..d60a5b239 100644 --- a/pkg/api/handlers/handler.go +++ b/pkg/api/handlers/handler.go @@ -15,10 +15,11 @@ func getVar(r *http.Request, k string) string { return mux.Vars(r)[k] } -func hasVar(r *http.Request, k string) bool { - _, found := mux.Vars(r)[k] - return found -} +// func hasVar(r *http.Request, k string) bool { +// _, found := mux.Vars(r)[k] +// return found +// } + func getName(r *http.Request) string { return getVar(r, "name") } diff --git a/pkg/api/handlers/images.go b/pkg/api/handlers/images.go index b4acdc312..3f66a63c8 100644 --- a/pkg/api/handlers/images.go +++ b/pkg/api/handlers/images.go @@ -3,7 +3,6 @@ package handlers import ( "fmt" "io" - "io/ioutil" "net/http" "os" "strconv" @@ -127,46 +126,6 @@ func GetImage(r *http.Request, name string) (*image.Image, error) { return runtime.ImageRuntime().NewFromLocal(name) } -func LoadImage(w http.ResponseWriter, r *http.Request) { - decoder := r.Context().Value("decoder").(*schema.Decoder) - runtime := r.Context().Value("runtime").(*libpod.Runtime) - - query := struct { - //quiet bool # quiet is currently unused - }{ - // 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 - } - - var ( - err error - writer io.Writer - ) - f, err := ioutil.TempFile("", "api_load.tar") - if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to create tempfile")) - return - } - if err := SaveFromBody(f, r); err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to write temporary file")) - return - } - id, err := runtime.LoadImage(r.Context(), "", f.Name(), writer, "") - if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to load image")) - return - } - utils.WriteResponse(w, http.StatusOK, struct { - Stream string `json:"stream"` - }{ - Stream: fmt.Sprintf("Loaded image: %s\n", id), - }) -} - func SaveFromBody(f *os.File, r *http.Request) error { // nolint if _, err := io.Copy(f, r.Body); err != nil { return err diff --git a/pkg/api/handlers/libpod/containers.go b/pkg/api/handlers/libpod/containers.go index df16843c7..db1cf26ff 100644 --- a/pkg/api/handlers/libpod/containers.go +++ b/pkg/api/handlers/libpod/containers.go @@ -19,8 +19,6 @@ func StopContainer(w http.ResponseWriter, r *http.Request) { } func ContainerExists(w http.ResponseWriter, r *http.Request) { - // 404 no such container - // 200 ok runtime := r.Context().Value("runtime").(*libpod.Runtime) name := mux.Vars(r)["name"] _, err := runtime.LookupContainer(name) @@ -147,10 +145,6 @@ func LogsFromContainer(w http.ResponseWriter, r *http.Request) { // tail string } -func CreateContainer(w http.ResponseWriter, r *http.Request) { - -} - func UnmountContainer(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) name := mux.Vars(r)["name"] diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index 0d4e220a8..9f0876618 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -1,9 +1,12 @@ package libpod import ( + "fmt" + "io" "io/ioutil" "net/http" "os" + "strconv" "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/api/handlers" @@ -42,12 +45,12 @@ func ImageExists(w http.ResponseWriter, r *http.Request) { func ImageTree(w http.ResponseWriter, r *http.Request) { // tree is a bit of a mess ... logic is in adapter and therefore not callable from here. needs rework - //name := mux.Vars(r)["name"] - //_, layerInfoMap, _, err := s.Runtime.Tree(name) - //if err != nil { + // name := mux.Vars(r)["name"] + // _, layerInfoMap, _, err := s.Runtime.Tree(name) + // if err != nil { // Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrapf(err, "Failed to find image information for %q", name)) // return - //} + // } // it is not clear to me how to deal with this given all the processing of the image // is in main. we need to discuss how that really should be and return something useful. handlers.UnsupportedHandler(w, r) @@ -90,13 +93,14 @@ func GetImages(w http.ResponseWriter, r *http.Request) { } func PruneImages(w http.ResponseWriter, r *http.Request) { - // 200 ok - // 500 internal + var ( + all bool + err error + ) runtime := r.Context().Value("runtime").(*libpod.Runtime) decoder := r.Context().Value("decoder").(*schema.Decoder) query := struct { - All bool `schema:"all"` - Filters []string `schema:"filters"` + Filters map[string][]string `schema:"filters"` }{ // override any golang type defaults } @@ -106,7 +110,19 @@ func PruneImages(w http.ResponseWriter, r *http.Request) { errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) return } - cids, err := runtime.ImageRuntime().PruneImages(r.Context(), query.All, query.Filters) + + var libpodFilters = []string{} + if _, found := r.URL.Query()["filters"]; found { + all, err = strconv.ParseBool(query.Filters["all"][0]) + if err != nil { + utils.InternalServerError(w, err) + return + } + for k, v := range query.Filters { + libpodFilters = append(libpodFilters, fmt.Sprintf("%s=%s", k, v[0])) + } + } + cids, err := runtime.ImageRuntime().PruneImages(r.Context(), all, libpodFilters) if err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) return @@ -163,3 +179,47 @@ func ExportImage(w http.ResponseWriter, r *http.Request) { defer os.Remove(tmpfile.Name()) utils.WriteResponse(w, http.StatusOK, rdr) } + +func ImportImage(w http.ResponseWriter, r *http.Request) { + // TODO this is basically wrong + decoder := r.Context().Value("decoder").(*schema.Decoder) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + query := struct { + Changes map[string]string `json:"changes"` + Message string `json:"message"` + Quiet bool `json:"quiet"` + }{ + // 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 + } + + var ( + err error + writer io.Writer + ) + f, err := ioutil.TempFile("", "api_load.tar") + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to create tempfile")) + return + } + if err := handlers.SaveFromBody(f, r); err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to write temporary file")) + return + } + id, err := runtime.LoadImage(r.Context(), "", f.Name(), writer, "") + //id, err := runtime.Import(r.Context()) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to load image")) + return + } + utils.WriteResponse(w, http.StatusOK, struct { + Stream string `json:"stream"` + }{ + Stream: fmt.Sprintf("Loaded image: %s\n", id), + }) +} diff --git a/pkg/api/handlers/libpod/pods.go b/pkg/api/handlers/libpod/pods.go index 14f8e8de7..50d763338 100644 --- a/pkg/api/handlers/libpod/pods.go +++ b/pkg/api/handlers/libpod/pods.go @@ -108,7 +108,7 @@ func Pods(w http.ResponseWriter, r *http.Request) { ) decoder := r.Context().Value("decoder").(*schema.Decoder) query := struct { - filters []string `schema:"filters"` + Filters map[string][]string `schema:"filters"` }{ // override any golang type defaults } @@ -117,10 +117,12 @@ func Pods(w http.ResponseWriter, r *http.Request) { errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) return } - if len(query.filters) > 0 { + + if _, found := r.URL.Query()["filters"]; found { utils.Error(w, "filters are not implemented yet", http.StatusInternalServerError, define.ErrNotImplemented) return } + pods, err := runtime.GetAllPods() if err != nil { utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) @@ -164,7 +166,7 @@ func PodStop(w http.ResponseWriter, r *http.Request) { decoder = r.Context().Value("decoder").(*schema.Decoder) ) query := struct { - timeout int `schema:"t"` + Timeout int `schema:"t"` }{ // override any golang type defaults } @@ -207,8 +209,8 @@ func PodStop(w http.ResponseWriter, r *http.Request) { return } - if query.timeout > 0 { - _, stopError = pod.StopWithTimeout(r.Context(), false, query.timeout) + if query.Timeout > 0 { + _, stopError = pod.StopWithTimeout(r.Context(), false, query.Timeout) } else { _, stopError = pod.Stop(r.Context(), false) } @@ -266,7 +268,7 @@ func PodDelete(w http.ResponseWriter, r *http.Request) { decoder = r.Context().Value("decoder").(*schema.Decoder) ) query := struct { - force bool `schema:"force"` + Force bool `schema:"force"` }{ // override any golang type defaults } @@ -282,7 +284,7 @@ func PodDelete(w http.ResponseWriter, r *http.Request) { utils.PodNotFound(w, name, err) return } - if err := runtime.RemovePod(r.Context(), pod, true, query.force); err != nil { + if err := runtime.RemovePod(r.Context(), pod, true, query.Force); err != nil { utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) return } @@ -307,43 +309,14 @@ func PodRestart(w http.ResponseWriter, r *http.Request) { func PodPrune(w http.ResponseWriter, r *http.Request) { var ( - err error - pods []*libpod.Pod runtime = r.Context().Value("runtime").(*libpod.Runtime) - decoder = r.Context().Value("decoder").(*schema.Decoder) ) - query := struct { - force bool `schema:"force"` - }{ - // override any golang type defaults - } - - if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, - errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) - return - } - - if query.force { - pods, err = runtime.GetAllPods() - if err != nil { - utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) - return - } - } else { - // TODO We need to make a libpod.PruneVolumes or this code will be a mess. Volumes - // already does this right. It will also help clean this code path up with less - // conditionals. We do this when we integrate with libpod again. - utils.Error(w, "not implemented", http.StatusInternalServerError, errors.New("not implemented")) + pruned, err := runtime.PrunePods() + if err != nil { + utils.InternalServerError(w, err) return } - for _, p := range pods { - if err := runtime.RemovePod(r.Context(), p, true, query.force); err != nil { - utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) - return - } - } - utils.WriteResponse(w, http.StatusNoContent, "") + utils.WriteResponse(w, http.StatusOK, pruned) } func PodPause(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go index 33cd51164..33cf1e95d 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -537,3 +537,11 @@ func portsToPortSet(input map[string]struct{}) (nat.PortSet, error) { } return ports, nil } + +// ContainerCreateResponse is the response struct for creating a container +type ContainerCreateResponse struct { + // ID of the container created + ID string `json:"id"` + // Warnings during container creation + Warnings []string `json:"Warnings"` +} diff --git a/pkg/api/handlers/utils/errors.go b/pkg/api/handlers/utils/errors.go index 9d2081cd8..8d499f40b 100644 --- a/pkg/api/handlers/utils/errors.go +++ b/pkg/api/handlers/utils/errors.go @@ -21,8 +21,9 @@ func Error(w http.ResponseWriter, apiMessage string, code int, err error) { // Log detailed message of what happened to machine running podman service log.Infof("Request Failed(%s): %s", http.StatusText(code), err.Error()) em := ErrorModel{ - Because: (errors.Cause(err)).Error(), - Message: err.Error(), + Because: (errors.Cause(err)).Error(), + Message: err.Error(), + ResponseCode: code, } WriteJSON(w, code, em) } @@ -79,6 +80,8 @@ type ErrorModel struct { // human error message, formatted for a human to read // example: human error message Message string `json:"message"` + // http response code + ResponseCode int `json:"response"` } func (e ErrorModel) Error() string { @@ -89,6 +92,10 @@ func (e ErrorModel) Cause() error { return errors.New(e.Because) } +func (e ErrorModel) Code() int { + return e.ResponseCode +} + // UnsupportedParameter logs a given param by its string name as not supported. func UnSupportedParameter(param string) { log.Infof("API parameter %q: not supported", param) diff --git a/pkg/api/handlers/utils/images.go b/pkg/api/handlers/utils/images.go index 9445298ca..2b651584a 100644 --- a/pkg/api/handlers/utils/images.go +++ b/pkg/api/handlers/utils/images.go @@ -6,6 +6,7 @@ import ( "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/image" + "github.com/gorilla/mux" "github.com/gorilla/schema" ) @@ -15,17 +16,23 @@ func GetImages(w http.ResponseWriter, r *http.Request) ([]*image.Image, error) { decoder := r.Context().Value("decoder").(*schema.Decoder) runtime := r.Context().Value("runtime").(*libpod.Runtime) query := struct { - //all bool # all is currently unused - filters []string - //digests bool # digests is currently unused + All bool + Filters map[string][]string `schema:"filters"` + Digests bool }{ // This is where you can override the golang default value for one of fields } + // TODO I think all is implemented with a filter? + if err := decoder.Decode(&query, r.URL.Query()); err != nil { return nil, err } - filters := query.filters - if len(filters) < 1 { + var filters = []string{} + if _, found := mux.Vars(r)["digests"]; found && query.Digests { + UnSupportedParameter("digests") + } + + if _, found := r.URL.Query()["filters"]; found { filters = append(filters, fmt.Sprintf("reference=%s", "")) } return runtime.ImageRuntime().GetImagesWithFilters(filters) diff --git a/pkg/api/handlers/generic/version.go b/pkg/api/handlers/version.go index 39423914d..94166952c 100644 --- a/pkg/api/handlers/generic/version.go +++ b/pkg/api/handlers/version.go @@ -1,4 +1,4 @@ -package generic +package handlers import ( "fmt" @@ -8,7 +8,6 @@ import ( "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" - "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" docker "github.com/docker/docker/api/types" "github.com/pkg/errors" @@ -53,7 +52,7 @@ func VersionHandler(w http.ResponseWriter, r *http.Request) { }, }} - utils.WriteResponse(w, http.StatusOK, handlers.Version{Version: docker.Version{ + utils.WriteResponse(w, http.StatusOK, Version{Version: docker.Version{ Platform: struct { Name string }{ diff --git a/pkg/api/server/docs.go b/pkg/api/server/docs.go index e028c6302..c989c7927 100644 --- a/pkg/api/server/docs.go +++ b/pkg/api/server/docs.go @@ -12,7 +12,8 @@ // Version: 0.0.1 // License: Apache-2.0 https://opensource.org/licenses/Apache-2.0 // Contact: Podman <podman@lists.podman.io> https://podman.io/community/ -// Extensions: +// +// InfoExtensions: // x-logo: // - url: https://raw.githubusercontent.com/containers/libpod/master/logo/podman-logo.png // - altText: "Podman logo" diff --git a/pkg/api/server/register_containers.go b/pkg/api/server/register_containers.go index b2d2ab388..abae81c38 100644 --- a/pkg/api/server/register_containers.go +++ b/pkg/api/server/register_containers.go @@ -33,7 +33,7 @@ func (s *APIServer) RegisterContainersHandlers(r *mux.Router) error { // $ref: "#/responses/ConflictError" // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/containers/create"), APIHandler(s.Context, generic.CreateContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/containers/create"), APIHandler(s.Context, handlers.CreateContainer)).Methods(http.MethodPost) // swagger:operation GET /containers/json compat listContainers // --- // tags: @@ -550,7 +550,7 @@ func (s *APIServer) RegisterContainersHandlers(r *mux.Router) error { libpod endpoints */ - r.HandleFunc(VersionedPath("/libpod/containers/create"), APIHandler(s.Context, libpod.CreateContainer)).Methods(http.MethodPost) + r.HandleFunc(VersionedPath("/libpod/containers/create"), APIHandler(s.Context, handlers.CreateContainer)).Methods(http.MethodPost) // swagger:operation GET /libpod/containers/json libpod libpodListContainers // --- // tags: diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go index 6e8b79313..f1cc0574c 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -55,6 +55,27 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // - images (compat) // summary: List Images // description: Returns a list of images on the server. Note that it uses a different, smaller representation of an image than inspecting a single image. + // parameters: + // - name: all + // in: query + // description: "Show all images. Only images from a final layer (no children) are shown by default." + // type: boolean + // default: false + // - name: filters + // in: query + // description: | + // A JSON encoded value of the filters (a `map[string][]string`) to process on the images list. Available filters: + // - `before`=(`<image-name>[:<tag>]`, `<image id>` or `<image@digest>`) + // - `dangling=true` + // - `label=key` or `label="key=value"` of an image label + // - `reference`=(`<image-name>[:<tag>]`) + // - `since`=(`<image-name>[:<tag>]`, `<image id>` or `<image@digest>`) + // type: string + // - name: digests + // in: query + // description: Not supported + // type: boolean + // default: false // produces: // - application/json // responses: @@ -63,7 +84,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // 500: // $ref: '#/responses/InternalError' r.Handle(VersionedPath("/images/json"), APIHandler(s.Context, generic.GetImages)).Methods(http.MethodGet) - // swagger:operation POST /images/load compat loadImage + // swagger:operation POST /images/load compat importImage // --- // tags: // - images (compat) @@ -86,7 +107,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // description: no error // 500: // $ref: '#/responses/InternalError' - r.Handle(VersionedPath("/images/load"), APIHandler(s.Context, handlers.LoadImage)).Methods(http.MethodPost) + r.Handle(VersionedPath("/images/load"), APIHandler(s.Context, libpod.ImportImage)).Methods(http.MethodPost) // swagger:operation POST /images/prune compat pruneImages // --- // tags: @@ -174,7 +195,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // $ref: '#/responses/ConflictError' // 500: // $ref: '#/responses/InternalError' - r.Handle(VersionedPath("/images/name"), APIHandler(s.Context, handlers.RemoveImage)).Methods(http.MethodDelete) + r.Handle(VersionedPath("/images/{name}"), APIHandler(s.Context, handlers.RemoveImage)).Methods(http.MethodDelete) // swagger:operation GET /images/{name}/get compat exportImage // --- // tags: @@ -585,6 +606,22 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // - images // summary: List Images // description: Returns a list of images on the server + // parameters: + // - name: all + // in: query + // description: Show all images. Only images from a final layer (no children) are shown by default. + // type: boolean + // default: false + // - name: filters + // in: query + // description: | + // A JSON encoded value of the filters (a `map[string][]string`) to process on the images list. Available filters: + // - `before`=(`<image-name>[:<tag>]`, `<image id>` or `<image@digest>`) + // - `dangling=true` + // - `label=key` or `label="key=value"` of an image label + // - `reference`=(`<image-name>[:<tag>]`) + // - `since`=(`<image-name>[:<tag>]`, `<image id>` or `<image@digest>`) + // type: string // produces: // - application/json // responses: @@ -593,7 +630,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // 500: // $ref: '#/responses/InternalError' r.Handle(VersionedPath("/libpod/images/json"), APIHandler(s.Context, libpod.GetImages)).Methods(http.MethodGet) - // swagger:operation POST /libpod/images/load libpod libpodLoadImage + // swagger:operation POST /libpod/images/load libpod libpodImportImage // --- // tags: // - images @@ -604,6 +641,14 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // name: quiet // type: boolean // description: not supported + // - in: query + // name: change + // description: "Apply the following possible instructions to the created image (default []): CMD | ENTRYPOINT | ENV | EXPOSE | LABEL | STOPSIGNAL | USER | VOLUME | WORKDIR. JSON encoded string" + // type: string + // - in: query + // name: message + // description: Set commit message for imported image + // type: string // - in: body // name: request // description: tarball of container image @@ -617,7 +662,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // description: no error // 500: // $ref: '#/responses/InternalError' - r.Handle(VersionedPath("/libpod/images/load"), APIHandler(s.Context, handlers.LoadImage)).Methods(http.MethodPost) + r.Handle(VersionedPath("/libpod/images/load"), APIHandler(s.Context, libpod.ImportImage)).Methods(http.MethodPost) // swagger:operation POST /libpod/images/prune libpod libpodPruneImages // --- // tags: @@ -635,10 +680,6 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // (or `0`), all unused images are pruned. // - `until=<string>` Prune images created before this timestamp. The `<timestamp>` can be Unix timestamps, date formatted timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed relative to the daemon machine’s time. // - `label` (`label=<key>`, `label=<key>=<value>`, `label!=<key>`, or `label!=<key>=<value>`) Prune images with (or without, in case `label!=...` is used) the specified labels. - // - in: query - // name: all - // type: boolean - // description: prune all images // produces: // - application/json // responses: @@ -707,7 +748,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // $ref: '#/responses/ConflictError' // 500: // $ref: '#/responses/InternalError' - r.Handle(VersionedPath("/libpod/images/name"), APIHandler(s.Context, handlers.RemoveImage)).Methods(http.MethodDelete) + r.Handle(VersionedPath("/libpod/images/{name}"), APIHandler(s.Context, handlers.RemoveImage)).Methods(http.MethodDelete) // swagger:operation GET /libpod/images/{name}/get libpod libpoodExportImage // --- // tags: diff --git a/pkg/api/server/register_pods.go b/pkg/api/server/register_pods.go index 1ef14b58c..5c7b51871 100644 --- a/pkg/api/server/register_pods.go +++ b/pkg/api/server/register_pods.go @@ -30,17 +30,15 @@ func (s *APIServer) registerPodsHandlers(r *mux.Router) error { // swagger:operation POST /libpod/pods/prune pods PrunePods // --- // summary: Prune unused pods - // parameters: - // - in: query - // name: force - // description: force delete - // type: boolean - // default: false // produces: // - application/json // responses: - // 204: - // description: no error + // 200: + // description: tbd + // schema: + // type: object + // additionalProperties: + // type: string // 400: // $ref: "#/responses/BadParamError" // 500: @@ -60,7 +58,7 @@ func (s *APIServer) registerPodsHandlers(r *mux.Router) error { // - in: query // name: force // type: boolean - // description: force delete + // description : force removal of a running pod by first stopping all containers, then removing all containers in the pod // responses: // 204: // description: no error diff --git a/pkg/api/server/register_version.go b/pkg/api/server/register_version.go index 94216b1b6..d3b47c2a9 100644 --- a/pkg/api/server/register_version.go +++ b/pkg/api/server/register_version.go @@ -1,12 +1,12 @@ package server import ( - "github.com/containers/libpod/pkg/api/handlers/generic" + "github.com/containers/libpod/pkg/api/handlers" "github.com/gorilla/mux" ) func (s *APIServer) registerVersionHandlers(r *mux.Router) error { - r.Handle("/version", APIHandler(s.Context, generic.VersionHandler)) - r.Handle(VersionedPath("/version"), APIHandler(s.Context, generic.VersionHandler)) + r.Handle("/version", APIHandler(s.Context, handlers.VersionHandler)) + r.Handle(VersionedPath("/version"), APIHandler(s.Context, handlers.VersionHandler)) return nil } diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go index 847d11c3c..8c940763e 100644 --- a/pkg/api/server/server.go +++ b/pkg/api/server/server.go @@ -11,6 +11,7 @@ import ( "time" "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers" "github.com/coreos/go-systemd/activation" "github.com/gorilla/mux" "github.com/gorilla/schema" @@ -71,7 +72,7 @@ func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Li ReadTimeout: 20 * time.Second, WriteTimeout: 2 * time.Minute, }, - Decoder: schema.NewDecoder(), + Decoder: handlers.NewAPIDecoder(), Context: nil, Runtime: runtime, Listener: *listener, @@ -85,6 +86,7 @@ func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Li }) ctx, cancelFn := context.WithCancel(context.Background()) + server.CancelFunc = cancelFn // TODO: Use ConnContext when ported to go 1.13 ctx = context.WithValue(ctx, "decoder", server.Decoder) @@ -92,9 +94,6 @@ func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Li ctx = context.WithValue(ctx, "shutdownFunc", server.Shutdown) server.Context = ctx - server.CancelFunc = cancelFn - server.Decoder.IgnoreUnknownKeys(true) - router.NotFoundHandler = http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { // We can track user errors... diff --git a/pkg/bindings/connection.go b/pkg/bindings/connection.go index 551a63c62..3dec6ca20 100644 --- a/pkg/bindings/connection.go +++ b/pkg/bindings/connection.go @@ -1,14 +1,22 @@ package bindings import ( + "context" "fmt" "io" + "net" "net/http" + "net/url" + "path/filepath" + "strings" + + "github.com/containers/libpod/pkg/api/handlers" + jsoniter "github.com/json-iterator/go" + "github.com/pkg/errors" ) -const ( - defaultConnection string = "http://localhost:8080/v1.24/libpod" - pingConnection string = "http://localhost:8080/_ping" +var ( + defaultConnectionPath string = filepath.Join(fmt.Sprintf("v%s", handlers.MinimalApiVersion), "libpod") ) type APIResponse struct { @@ -17,46 +25,170 @@ type APIResponse struct { } type Connection struct { - url string - client *http.Client + scheme string + address string + client *http.Client } -func NewConnection(url string) (Connection, error) { - if len(url) < 1 { - url = defaultConnection +// NewConnection takes a URI as a string and returns a context with the +// Connection embedded as a value. This context needs to be passed to each +// endpoint to work correctly. +// +// A valid URI connection should be scheme:// +// For example tcp://localhost:<port> +// or unix://run/podman/podman.sock +func NewConnection(uri string) (context.Context, error) { + u, err := url.Parse(uri) + if err != nil { + return nil, err } - newConn := Connection{ - url: url, - client: &http.Client{}, + // TODO once ssh is implemented, remove this block and + // add it to the conditional beneath it + if u.Scheme == "ssh" { + return nil, ErrNotImplemented + } + if u.Scheme != "tcp" && u.Scheme != "unix" { + return nil, errors.Errorf("%s is not a support schema", u.Scheme) + } + + if u.Scheme == "tcp" && !strings.HasPrefix(uri, "tcp://") { + return nil, errors.New("tcp URIs should begin with tcp://") + } + + address := u.Path + if u.Scheme == "tcp" { + address = u.Host + } + newConn := newConnection(u.Scheme, address) + ctx := context.WithValue(context.Background(), "conn", &newConn) + if err := pingNewConnection(ctx); err != nil { + return nil, err } - response, err := http.Get(pingConnection) + return ctx, nil +} + +// pingNewConnection pings to make sure the RESTFUL service is up +// and running. it should only be used where initializing a connection +func pingNewConnection(ctx context.Context) error { + conn, err := GetConnectionFromContext(ctx) if err != nil { - return newConn, err + return err + } + // the ping endpoint sits at / in this case + response, err := conn.DoRequest(nil, http.MethodGet, "../../../_ping", nil) + if err != nil { + return err + } + if response.StatusCode == http.StatusOK { + return nil } - if err := response.Body.Close(); err != nil { - return newConn, err + return errors.Errorf("ping response was %q", response.StatusCode) +} + +// newConnection takes a scheme and address and creates a connection from it +func newConnection(scheme, address string) Connection { + client := http.Client{ + Transport: &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial(scheme, address) + }, + }, } - return newConn, err + newConn := Connection{ + client: &client, + address: address, + scheme: scheme, + } + return newConn } -func (c Connection) makeEndpoint(u string) string { - return fmt.Sprintf("%s%s", defaultConnection, u) +func (c *Connection) makeEndpoint(u string) string { + // The d character in the url is discarded and is meaningless + return fmt.Sprintf("http://d/%s%s", defaultConnectionPath, u) } -func (c Connection) newRequest(httpMethod, endpoint string, httpBody io.Reader, params map[string]string) (*APIResponse, error) { - e := c.makeEndpoint(endpoint) +// DoRequest assembles the http request and returns the response +func (c *Connection) DoRequest(httpBody io.Reader, httpMethod, endpoint string, queryParams map[string]string, pathValues ...string) (*APIResponse, error) { + var ( + err error + response *http.Response + ) + safePathValues := make([]interface{}, len(pathValues)) + // Make sure path values are http url safe + for _, pv := range pathValues { + safePathValues = append(safePathValues, url.QueryEscape(pv)) + } + safeEndpoint := fmt.Sprintf(endpoint, safePathValues...) + + e := c.makeEndpoint(safeEndpoint) req, err := http.NewRequest(httpMethod, e, httpBody) if err != nil { return nil, err } - if len(params) > 0 { + if len(queryParams) > 0 { // if more desirable we could use url to form the encoded endpoint with params r := req.URL.Query() - for k, v := range params { - r.Add(k, v) + for k, v := range queryParams { + r.Add(k, url.QueryEscape(v)) } req.URL.RawQuery = r.Encode() } - response, err := c.client.Do(req) // nolint + // Give the Do three chances in the case of a comm/service hiccup + for i := 0; i < 3; i++ { + response, err = c.client.Do(req) // nolint + if err == nil { + break + } + } return &APIResponse{response, req}, err } + +// GetConnectionFromContext returns a bindings connection from the context +// being passed into each method. +func GetConnectionFromContext(ctx context.Context) (*Connection, error) { + c := ctx.Value("conn") + if c == nil { + return nil, errors.New("unable to get connection from context") + } + conn := c.(Connection) + return &conn, nil +} + +// FiltersToHTML converts our typical filter format of a +// map[string][]string to a query/html safe string. +func FiltersToHTML(filters map[string][]string) (string, error) { + lowerCaseKeys := make(map[string][]string) + for k, v := range filters { + lowerCaseKeys[strings.ToLower(k)] = v + } + unsafeString, err := jsoniter.MarshalToString(lowerCaseKeys) + if err != nil { + return "", err + } + return url.QueryEscape(unsafeString), nil +} + +// IsInformation returns true if the response code is 1xx +func (h *APIResponse) IsInformational() bool { + return h.Response.StatusCode/100 == 1 +} + +// IsSuccess returns true if the response code is 2xx +func (h *APIResponse) IsSuccess() bool { + return h.Response.StatusCode/100 == 2 +} + +// IsRedirection returns true if the response code is 3xx +func (h *APIResponse) IsRedirection() bool { + return h.Response.StatusCode/100 == 3 +} + +// IsClientError returns true if the response code is 4xx +func (h *APIResponse) IsClientError() bool { + return h.Response.StatusCode/100 == 4 +} + +// IsServerError returns true if the response code is 5xx +func (h *APIResponse) IsServerError() bool { + return h.Response.StatusCode/100 == 5 +} diff --git a/pkg/bindings/containers.go b/pkg/bindings/containers.go deleted file mode 100644 index 057580088..000000000 --- a/pkg/bindings/containers.go +++ /dev/null @@ -1,139 +0,0 @@ -package bindings - -import ( - "fmt" - "net/http" - "strconv" - - "github.com/containers/libpod/cmd/podman/shared" - "github.com/containers/libpod/libpod" -) - -func (c Connection) ListContainers(filter []string, last int, size, sync bool) ([]shared.PsContainerOutput, error) { // nolint:typecheck - images := []shared.PsContainerOutput{} - params := make(map[string]string) - params["last"] = strconv.Itoa(last) - params["size"] = strconv.FormatBool(size) - params["sync"] = strconv.FormatBool(sync) - response, err := c.newRequest(http.MethodGet, "/containers/json", nil, params) - if err != nil { - return images, err - } - return images, response.Process(nil) -} - -func (c Connection) PruneContainers() ([]string, error) { - var ( - pruned []string - ) - response, err := c.newRequest(http.MethodPost, "/containers/prune", nil, nil) - if err != nil { - return pruned, err - } - return pruned, response.Process(nil) -} - -func (c Connection) RemoveContainer(nameOrID string, force, volumes bool) error { - params := make(map[string]string) - params["force"] = strconv.FormatBool(force) - params["vols"] = strconv.FormatBool(volumes) - response, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/containers/%s", nameOrID), nil, params) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) InspectContainer(nameOrID string, size bool) (*libpod.InspectContainerData, error) { - params := make(map[string]string) - params["size"] = strconv.FormatBool(size) - response, err := c.newRequest(http.MethodGet, fmt.Sprintf("/containers/%s/json", nameOrID), nil, params) - if err != nil { - return nil, err - } - inspect := libpod.InspectContainerData{} - return &inspect, response.Process(&inspect) -} - -func (c Connection) KillContainer(nameOrID string, signal int) error { - params := make(map[string]string) - params["signal"] = strconv.Itoa(signal) - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/containers/%s/kill", nameOrID), nil, params) - if err != nil { - return err - } - return response.Process(nil) - -} -func (c Connection) ContainerLogs() {} -func (c Connection) PauseContainer(nameOrID string) error { - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/containers/%s/pause", nameOrID), nil, nil) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) RestartContainer(nameOrID string, timeout int) error { - // TODO how do we distinguish between an actual zero value and not wanting to change the timeout value - params := make(map[string]string) - params["timeout"] = strconv.Itoa(timeout) - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/containers/%s/restart", nameOrID), nil, params) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) StartContainer(nameOrID, detachKeys string) error { - params := make(map[string]string) - if len(detachKeys) > 0 { - params["detachKeys"] = detachKeys - } - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/containers/%s/start", nameOrID), nil, params) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) ContainerStats() {} -func (c Connection) ContainerTop() {} - -func (c Connection) UnpauseContainer(nameOrID string) error { - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/containers/%s/unpause", nameOrID), nil, nil) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) WaitContainer(nameOrID string) error { - // TODO when returns are ironed out, we can should use the newRequest approach - _, err := http.Post(c.makeEndpoint(fmt.Sprintf("containers/%s/wait", nameOrID)), "application/json", nil) // nolint - return err -} - -func (c Connection) ContainerExists(nameOrID string) (bool, error) { - response, err := http.Get(c.makeEndpoint(fmt.Sprintf("/containers/%s/exists", nameOrID))) // nolint - defer closeResponseBody(response) - if err != nil { - return false, err - } - if response.StatusCode == http.StatusOK { - return true, nil - } - return false, nil -} - -func (c Connection) StopContainer(nameOrID string, timeout *int) error { - params := make(map[string]string) - if timeout != nil { - params["t"] = strconv.Itoa(*timeout) - } - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/containers/%s/stop", nameOrID), nil, params) - if err != nil { - return err - } - return response.Process(nil) -} diff --git a/pkg/bindings/containers/containers.go b/pkg/bindings/containers/containers.go new file mode 100644 index 000000000..334a656d4 --- /dev/null +++ b/pkg/bindings/containers/containers.go @@ -0,0 +1,255 @@ +package containers + +import ( + "context" + "net/http" + "strconv" + + "github.com/containers/libpod/cmd/podman/shared" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/bindings" +) + +// List obtains a list of containers in local storage. All parameters to this method are optional. +// The filters are used to determine which containers are listed. The last parameter indicates to only return +// the most recent number of containers. The pod and size booleans indicate that pod information and rootfs +// size information should also be included. Finally, the sync bool synchronizes the OCI runtime and +// container state. +func List(ctx context.Context, filters map[string][]string, last *int, pod, size, sync *bool) ([]*shared.PsContainerOutput, error) { // nolint:typecheck + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + var images []*shared.PsContainerOutput + params := make(map[string]string) + if last != nil { + params["last"] = strconv.Itoa(*last) + } + if pod != nil { + params["pod"] = strconv.FormatBool(*pod) + } + if size != nil { + params["size"] = strconv.FormatBool(*size) + } + if sync != nil { + params["sync"] = strconv.FormatBool(*sync) + } + if filters != nil { + filterString, err := bindings.FiltersToHTML(filters) + if err != nil { + return nil, err + } + params["filters"] = filterString + } + response, err := conn.DoRequest(nil, http.MethodGet, "/containers/json", params) + if err != nil { + return images, err + } + return images, response.Process(nil) +} + +// Prune removes stopped and exited containers from local storage. The optional filters can be +// used for more granular selection of containers. The main error returned indicates if there were runtime +// errors like finding containers. Errors specific to the removal of a container are in the PruneContainerResponse +// structure. +func Prune(ctx context.Context, filters map[string][]string) ([]string, error) { + var ( + pruneResponse []string + ) + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + params := make(map[string]string) + if filters != nil { + filterString, err := bindings.FiltersToHTML(filters) + if err != nil { + return nil, err + } + params["filters"] = filterString + } + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/prune", params) + if err != nil { + return pruneResponse, err + } + return pruneResponse, response.Process(pruneResponse) +} + +// Remove removes a container from local storage. The force bool designates +// that the container should be removed forcibly (example, even it is running). The volumes +// bool dictates that a container's volumes should also be removed. +func Remove(ctx context.Context, nameOrID string, force, volumes *bool) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + params := make(map[string]string) + if force != nil { + params["force"] = strconv.FormatBool(*force) + } + if volumes != nil { + params["vols"] = strconv.FormatBool(*volumes) + } + response, err := conn.DoRequest(nil, http.MethodDelete, "/containers/%s", params, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +// Inspect returns low level information about a Container. The nameOrID can be a container name +// or a partial/full ID. The size bool determines whether the size of the container's root filesystem +// should be calculated. Calculating the size of a container requires extra work from the filesystem and +// is therefore slower. +func Inspect(ctx context.Context, nameOrID string, size *bool) (*libpod.InspectContainerData, error) { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + params := make(map[string]string) + if size != nil { + params["size"] = strconv.FormatBool(*size) + } + response, err := conn.DoRequest(nil, http.MethodGet, "/containers/%s/json", params, nameOrID) + if err != nil { + return nil, err + } + inspect := libpod.InspectContainerData{} + return &inspect, response.Process(&inspect) +} + +// Kill sends a given signal to a given container. The signal should be the string +// representation of a signal like 'SIGKILL'. The nameOrID can be a container name +// or a partial/full ID +func Kill(ctx context.Context, nameOrID string, signal string) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + params := make(map[string]string) + params["signal"] = signal + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/kill", params, nameOrID) + if err != nil { + return err + } + return response.Process(nil) + +} +func Logs() {} + +// Pause pauses a given container. The nameOrID can be a container name +// or a partial/full ID. +func Pause(ctx context.Context, nameOrID string) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/pause", nil, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +// Restart restarts a running container. The nameOrID can be a container name +// or a partial/full ID. The optional timeout specifies the number of seconds to wait +// for the running container to stop before killing it. +func Restart(ctx context.Context, nameOrID string, timeout *int) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + params := make(map[string]string) + if timeout != nil { + params["t"] = strconv.Itoa(*timeout) + } + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/restart", params, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +// Start starts a non-running container.The nameOrID can be a container name +// or a partial/full ID. The optional parameter for detach keys are to override the default +// detach key sequence. +func Start(ctx context.Context, nameOrID string, detachKeys *string) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + params := make(map[string]string) + if detachKeys != nil { + params["detachKeys"] = *detachKeys + } + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/start", params, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +func Stats() {} +func Top() {} + +// Unpause resumes the given paused container. The nameOrID can be a container name +// or a partial/full ID. +func Unpause(ctx context.Context, nameOrID string) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/unpause", nil, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +// Wait blocks until the given container exits and returns its exit code. The nameOrID can be a container name +// or a partial/full ID. +func Wait(ctx context.Context, nameOrID string) (int32, error) { + var exitCode int32 + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return exitCode, err + } + response, err := conn.DoRequest(nil, http.MethodPost, "containers/%s/wait", nil, nameOrID) + if err != nil { + return exitCode, err + } + return exitCode, response.Process(&exitCode) +} + +// Exists is a quick, light-weight way to determine if a given container +// exists in local storage. The nameOrID can be a container name +// or a partial/full ID. +func Exists(ctx context.Context, nameOrID string) (bool, error) { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return false, err + } + response, err := conn.DoRequest(nil, http.MethodGet, "containers/%s/exists", nil, nameOrID) + if err != nil { + return false, err + } + return response.IsSuccess(), nil +} + +// Stop stops a running container. The timeout is optional. The nameOrID can be a container name +// or a partial/full ID +func Stop(ctx context.Context, nameOrID string, timeout *int) error { + params := make(map[string]string) + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + if timeout != nil { + params["t"] = strconv.Itoa(*timeout) + } + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/stop", params, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} diff --git a/pkg/bindings/containers/healthcheck.go b/pkg/bindings/containers/healthcheck.go new file mode 100644 index 000000000..9ed7f858d --- /dev/null +++ b/pkg/bindings/containers/healthcheck.go @@ -0,0 +1,26 @@ +package containers + +import ( + "context" + "github.com/containers/libpod/pkg/bindings" + "net/http" + + "github.com/containers/libpod/libpod" +) + +// RunHealthCheck executes the container's healthcheck and returns the health status of the +// container. +func RunHealthCheck(ctx context.Context, nameOrID string) (*libpod.HealthCheckStatus, error) { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + var ( + status libpod.HealthCheckStatus + ) + response, err := conn.DoRequest(nil, http.MethodGet, "/containers/%s/runhealthcheck", nil, nameOrID) + if err != nil { + return nil, err + } + return &status, response.Process(&status) +} diff --git a/pkg/bindings/containers/mount.go b/pkg/bindings/containers/mount.go new file mode 100644 index 000000000..d68dee981 --- /dev/null +++ b/pkg/bindings/containers/mount.go @@ -0,0 +1,53 @@ +package containers + +import ( + "context" + "net/http" + + "github.com/containers/libpod/pkg/bindings" +) + +// Mount mounts an existing container to the filesystem. It returns the path +// of the mounted container in string format. +func Mount(ctx context.Context, nameOrID string) (string, error) { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return "", err + } + var ( + path string + ) + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/mount", nil, nameOrID) + if err != nil { + return path, err + } + return path, response.Process(&path) +} + +// Unmount unmounts a container from the filesystem. The container must not be running +// or the unmount will fail. +func Unmount(ctx context.Context, nameOrID string) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/unmount", nil, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +// GetMountedContainerPaths returns a map of mounted containers and their mount locations. +func GetMountedContainerPaths(ctx context.Context) (map[string]string, error) { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + mounts := make(map[string]string) + response, err := conn.DoRequest(nil, http.MethodGet, "/containers/showmounted", nil) + if err != nil { + return mounts, err + } + return mounts, response.Process(&mounts) +} diff --git a/pkg/bindings/errors.go b/pkg/bindings/errors.go index 9a02925a3..8bd40f804 100644 --- a/pkg/bindings/errors.go +++ b/pkg/bindings/errors.go @@ -7,7 +7,6 @@ import ( "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/pkg/errors" - "github.com/sirupsen/logrus" ) var ( @@ -37,10 +36,10 @@ func (a APIResponse) Process(unmarshalInto interface{}) error { return handleError(data) } -func closeResponseBody(r *http.Response) { - if r != nil { - if err := r.Body.Close(); err != nil { - logrus.Error(errors.Wrap(err, "unable to close response body")) - } +func CheckResponseCode(inError error) (int, error) { + e, ok := inError.(utils.ErrorModel) + if !ok { + return -1, errors.New("error is not type ErrorModel") } + return e.Code(), nil } diff --git a/pkg/bindings/generate.go b/pkg/bindings/generate.go deleted file mode 100644 index 534909062..000000000 --- a/pkg/bindings/generate.go +++ /dev/null @@ -1,4 +0,0 @@ -package bindings - -func (c Connection) GenerateKube() {} -func (c Connection) GenerateSystemd() {} diff --git a/pkg/bindings/generate/generate.go b/pkg/bindings/generate/generate.go new file mode 100644 index 000000000..2916754b8 --- /dev/null +++ b/pkg/bindings/generate/generate.go @@ -0,0 +1,4 @@ +package generate + +func GenerateKube() {} +func GenerateSystemd() {} diff --git a/pkg/bindings/healthcheck.go b/pkg/bindings/healthcheck.go deleted file mode 100644 index 32515e332..000000000 --- a/pkg/bindings/healthcheck.go +++ /dev/null @@ -1,19 +0,0 @@ -package bindings - -import ( - "fmt" - "net/http" - - "github.com/containers/libpod/libpod" -) - -func (c Connection) RunHealthCheck(nameOrID string) (*libpod.HealthCheckStatus, error) { - var ( - status libpod.HealthCheckStatus - ) - response, err := c.newRequest(http.MethodGet, fmt.Sprintf("/containers/%s/runhealthcheck", nameOrID), nil, nil) - if err != nil { - return nil, err - } - return &status, response.Process(&status) -} diff --git a/pkg/bindings/images.go b/pkg/bindings/images.go deleted file mode 100644 index 3abc8c372..000000000 --- a/pkg/bindings/images.go +++ /dev/null @@ -1,111 +0,0 @@ -package bindings - -import ( - "fmt" - "io" - "net/http" - "strconv" - - "github.com/containers/libpod/pkg/api/handlers" - "github.com/containers/libpod/pkg/inspect" -) - -func (c Connection) ImageExists(nameOrID string) (bool, error) { - response, err := http.Get(c.makeEndpoint(fmt.Sprintf("/images/%s/exists", nameOrID))) // nolint - defer closeResponseBody(response) - if err != nil { - return false, err - } - if response.StatusCode == http.StatusOK { - return true, nil - } - return false, nil -} - -func (c Connection) ListImages() ([]handlers.ImageSummary, error) { - imageSummary := []handlers.ImageSummary{} - response, err := c.newRequest(http.MethodGet, "/images/json", nil, nil) - if err != nil { - return imageSummary, err - } - return imageSummary, response.Process(&imageSummary) -} - -func (c Connection) GetImage(nameOrID string) (*inspect.ImageData, error) { - inspectedData := inspect.ImageData{} - response, err := c.newRequest(http.MethodGet, fmt.Sprintf("/images/%s/json", nameOrID), nil, nil) - if err != nil { - return &inspectedData, err - } - return &inspectedData, response.Process(&inspectedData) -} - -func (c Connection) ImageTree(nameOrId string) error { - return ErrNotImplemented -} - -func (c Connection) ImageHistory(nameOrID string) ([]handlers.HistoryResponse, error) { - history := []handlers.HistoryResponse{} - response, err := c.newRequest(http.MethodGet, fmt.Sprintf("/images/%s/history", nameOrID), nil, nil) - if err != nil { - return history, err - } - return history, response.Process(&history) -} - -func (c Connection) LoadImage(r io.Reader) error { - // TODO this still needs error handling added - _, err := http.Post(c.makeEndpoint("/images/loads"), "application/json", r) //nolint - return err -} - -func (c Connection) RemoveImage(nameOrID string, force bool) ([]map[string]string, error) { - deletes := []map[string]string{} - params := make(map[string]string) - params["force"] = strconv.FormatBool(force) - response, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/images/%s", nameOrID), nil, params) - if err != nil { - return nil, err - } - return deletes, response.Process(&deletes) -} - -func (c Connection) ExportImage(nameOrID string, w io.Writer, format string, compress bool) error { - params := make(map[string]string) - params["format"] = format - params["compress"] = strconv.FormatBool(compress) - response, err := c.newRequest(http.MethodGet, fmt.Sprintf("/images/%s/get", nameOrID), nil, params) - if err != nil { - return err - } - if err := response.Process(nil); err != nil { - return err - } - _, err = io.Copy(w, response.Body) - return err -} - -func (c Connection) PruneImages(all bool, filters []string) ([]string, error) { - var ( - deleted []string - ) - params := make(map[string]string) - // FIXME How do we do []strings? - //params["filters"] = format - response, err := c.newRequest(http.MethodPost, "/images/prune", nil, params) - if err != nil { - return deleted, err - } - return deleted, response.Process(nil) -} - -func (c Connection) TagImage(nameOrID string) error { - var () - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/images/%s/tag", nameOrID), nil, nil) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) BuildImage(nameOrId string) {} diff --git a/pkg/bindings/images/images.go b/pkg/bindings/images/images.go new file mode 100644 index 000000000..deaf93f0e --- /dev/null +++ b/pkg/bindings/images/images.go @@ -0,0 +1,187 @@ +package images + +import ( + "context" + "io" + "net/http" + "strconv" + + "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/inspect" +) + +// Exists a lightweight way to determine if an image exists in local storage. It returns a +// boolean response. +func Exists(ctx context.Context, nameOrID string) (bool, error) { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return false, err + } + response, err := conn.DoRequest(nil, http.MethodGet, "/images/%s/exists", nil, nameOrID) + if err != nil { + return false, err + } + return response.IsSuccess(), nil +} + +// List returns a list of images in local storage. The all boolean and filters parameters are optional +// ways to alter the image query. +func List(ctx context.Context, all *bool, filters map[string][]string) ([]*handlers.ImageSummary, error) { + var imageSummary []*handlers.ImageSummary + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + params := make(map[string]string) + if all != nil { + params["all"] = strconv.FormatBool(*all) + } + if filters != nil { + strFilters, err := bindings.FiltersToHTML(filters) + if err != nil { + return nil, err + } + params["filters"] = strFilters + } + response, err := conn.DoRequest(nil, http.MethodGet, "/images/json", params) + if err != nil { + return imageSummary, err + } + return imageSummary, response.Process(&imageSummary) +} + +// Get performs an image inspect. To have the on-disk size of the image calculated, you can +// use the optional size parameter. +func GetImage(ctx context.Context, nameOrID string, size *bool) (*inspect.ImageData, error) { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + params := make(map[string]string) + if size != nil { + params["size"] = strconv.FormatBool(*size) + } + inspectedData := inspect.ImageData{} + response, err := conn.DoRequest(nil, http.MethodGet, "/images/%s/json", params, nameOrID) + if err != nil { + return &inspectedData, err + } + return &inspectedData, response.Process(&inspectedData) +} + +func ImageTree(ctx context.Context, nameOrId string) error { + return bindings.ErrNotImplemented +} + +// History returns the parent layers of an image. +func History(ctx context.Context, nameOrID string) ([]*handlers.HistoryResponse, error) { + var history []*handlers.HistoryResponse + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + response, err := conn.DoRequest(nil, http.MethodGet, "/images/%s/history", nil, nameOrID) + if err != nil { + return history, err + } + return history, response.Process(&history) +} + +func Load(ctx context.Context, r io.Reader) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + // TODO this still needs error handling added + //_, err := http.Post(c.makeEndpoint("/images/loads"), "application/json", r) //nolint + _ = conn + return bindings.ErrNotImplemented +} + +// Remove deletes an image from local storage. The optional force parameter will forcibly remove +// the image by removing all all containers, including those that are Running, first. +func Remove(ctx context.Context, nameOrID string, force *bool) ([]map[string]string, error) { + var deletes []map[string]string + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + params := make(map[string]string) + if force != nil { + params["force"] = strconv.FormatBool(*force) + } + response, err := conn.DoRequest(nil, http.MethodDelete, "/images/%s", params, nameOrID) + if err != nil { + return nil, err + } + return deletes, response.Process(&deletes) +} + +// Export saves an image from local storage as a tarball or image archive. The optional format +// parameter is used to change the format of the output. +func Export(ctx context.Context, nameOrID string, w io.Writer, format *string, compress *bool) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + params := make(map[string]string) + if format != nil { + params["format"] = *format + } + if compress != nil { + params["compress"] = strconv.FormatBool(*compress) + } + response, err := conn.DoRequest(nil, http.MethodGet, "/images/%s/get", params, nameOrID) + if err != nil { + return err + } + if err := response.Process(nil); err != nil { + return err + } + _, err = io.Copy(w, response.Body) + return err +} + +// Prune removes unused images from local storage. The optional filters can be used to further +// define which images should be pruned. +func Prune(ctx context.Context, filters map[string][]string) ([]string, error) { + var ( + deleted []string + ) + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + params := make(map[string]string) + if filters != nil { + stringFilter, err := bindings.FiltersToHTML(filters) + if err != nil { + return nil, err + } + params["filters"] = stringFilter + } + response, err := conn.DoRequest(nil, http.MethodPost, "/images/prune", params) + if err != nil { + return deleted, err + } + return deleted, response.Process(nil) +} + +// Tag adds an additional name to locally-stored image. Both the tag and repo parameters are required. +func Tag(ctx context.Context, nameOrID, tag, repo string) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + params := make(map[string]string) + params["tag"] = tag + params["repo"] = repo + response, err := conn.DoRequest(nil, http.MethodPost, "/images/%s/tag", params, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +func Build(nameOrId string) {} diff --git a/pkg/bindings/images/search.go b/pkg/bindings/images/search.go new file mode 100644 index 000000000..d98ddf18d --- /dev/null +++ b/pkg/bindings/images/search.go @@ -0,0 +1,40 @@ +package images + +import ( + "context" + "net/http" + "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.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + params := make(map[string]string) + params["term"] = term + if limit != nil { + params["limit"] = strconv.Itoa(*limit) + } + if filters != nil { + stringFilter, err := bindings.FiltersToHTML(filters) + if err != nil { + return nil, err + } + params["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/mount.go b/pkg/bindings/mount.go deleted file mode 100644 index 2e3d6d7f6..000000000 --- a/pkg/bindings/mount.go +++ /dev/null @@ -1,26 +0,0 @@ -package bindings - -import ( - "fmt" - "net/http" -) - -func (c Connection) MountContainer(nameOrID string) (string, error) { - var ( - path string - ) - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/containers/%s/mount", nameOrID), nil, nil) - if err != nil { - return path, err - } - return path, response.Process(&path) -} - -func (c Connection) GetMountedContainerPaths() (map[string]string, error) { - mounts := make(map[string]string) - response, err := c.newRequest(http.MethodGet, "/containers/showmounted", nil, nil) - if err != nil { - return mounts, err - } - return mounts, response.Process(&mounts) -} diff --git a/pkg/bindings/network.go b/pkg/bindings/network.go deleted file mode 100644 index 383615e5d..000000000 --- a/pkg/bindings/network.go +++ /dev/null @@ -1,37 +0,0 @@ -package bindings - -import ( - "fmt" - "net/http" - - "github.com/containernetworking/cni/libcni" -) - -func (c Connection) CreateNetwork() {} -func (c Connection) InspectNetwork(nameOrID string) (map[string]interface{}, error) { - n := make(map[string]interface{}) - response, err := c.newRequest(http.MethodGet, fmt.Sprintf("/networks/%s/json", nameOrID), nil, nil) - if err != nil { - return n, err - } - return n, response.Process(&n) -} - -func (c Connection) RemoveNetwork(nameOrID string) error { - response, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/networks/%s", nameOrID), nil, nil) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) ListNetworks() ([]*libcni.NetworkConfigList, error) { - var ( - netList []*libcni.NetworkConfigList - ) - response, err := c.newRequest(http.MethodGet, "/networks/json", nil, nil) - if err != nil { - return netList, err - } - return netList, response.Process(&netList) -} diff --git a/pkg/bindings/network/network.go b/pkg/bindings/network/network.go new file mode 100644 index 000000000..97bbb8c42 --- /dev/null +++ b/pkg/bindings/network/network.go @@ -0,0 +1,50 @@ +package network + +import ( + "context" + "net/http" + + "github.com/containernetworking/cni/libcni" + "github.com/containers/libpod/pkg/bindings" +) + +func Create() {} +func Inspect(ctx context.Context, nameOrID string) (map[string]interface{}, error) { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + n := make(map[string]interface{}) + response, err := conn.DoRequest(nil, http.MethodGet, "/networks/%s/json", nil, nameOrID) + if err != nil { + return n, err + } + return n, response.Process(&n) +} + +func Remove(ctx context.Context, nameOrID string) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + response, err := conn.DoRequest(nil, http.MethodDelete, "/networks/%s", nil, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +func List(ctx context.Context) ([]*libcni.NetworkConfigList, error) { + var ( + netList []*libcni.NetworkConfigList + ) + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + response, err := conn.DoRequest(nil, http.MethodGet, "/networks/json", nil) + if err != nil { + return netList, err + } + return netList, response.Process(&netList) +} diff --git a/pkg/bindings/play.go b/pkg/bindings/play.go deleted file mode 100644 index a9dee82b1..000000000 --- a/pkg/bindings/play.go +++ /dev/null @@ -1,3 +0,0 @@ -package bindings - -func (c Connection) PlayKube() {} diff --git a/pkg/bindings/play/play.go b/pkg/bindings/play/play.go new file mode 100644 index 000000000..a6f03cad2 --- /dev/null +++ b/pkg/bindings/play/play.go @@ -0,0 +1,7 @@ +package play + +import "github.com/containers/libpod/pkg/bindings" + +func PlayKube() error { + return bindings.ErrNotImplemented +} diff --git a/pkg/bindings/pods.go b/pkg/bindings/pods.go deleted file mode 100644 index 704d71477..000000000 --- a/pkg/bindings/pods.go +++ /dev/null @@ -1,129 +0,0 @@ -package bindings - -import ( - "fmt" - "net/http" - "strconv" - - "github.com/containers/libpod/libpod" -) - -func (c Connection) CreatePod() error { - // TODO - return ErrNotImplemented -} - -func (c Connection) PodExists(nameOrID string) (bool, error) { - response, err := http.Get(c.makeEndpoint(fmt.Sprintf("/pods/%s/exists", nameOrID))) // nolint - defer closeResponseBody(response) - if err != nil { - return false, err - } - return response.StatusCode == http.StatusOK, err -} - -func (c Connection) InspectPod(nameOrID string) (*libpod.PodInspect, error) { - inspect := libpod.PodInspect{} - response, err := c.newRequest(http.MethodGet, fmt.Sprintf("/pods/%s/json", nameOrID), nil, nil) - if err != nil { - return &inspect, err - } - return &inspect, response.Process(&inspect) -} - -func (c Connection) KillPod(nameOrID string, signal int) error { - params := make(map[string]string) - params["signal"] = strconv.Itoa(signal) - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/pods/%s/kill", nameOrID), nil, params) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) PausePod(nameOrID string) error { - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/pods/%s/pause", nameOrID), nil, nil) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) PrunePods(force bool) error { - params := make(map[string]string) - params["force"] = strconv.FormatBool(force) - response, err := c.newRequest(http.MethodPost, "/pods/prune", nil, params) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) ListPods(filters []string) (*[]libpod.PodInspect, error) { - var ( - inspect []libpod.PodInspect - ) - params := make(map[string]string) - // TODO I dont remember how to do this for []string{} - // FIXME - //params["filters"] = strconv.FormatBool(force) - response, err := c.newRequest(http.MethodPost, "/pods/json", nil, params) - if err != nil { - return &inspect, err - } - return &inspect, response.Process(&inspect) -} - -func (c Connection) RestartPod(nameOrID string) error { - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/pods/%s/restart", nameOrID), nil, nil) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) RemovePod(nameOrID string, force bool) error { - params := make(map[string]string) - params["force"] = strconv.FormatBool(force) - response, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/pods/%s", nameOrID), nil, params) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) StartPod(nameOrID string) error { - response, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/pods/%s/start", nameOrID), nil, nil) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) PodStats() error { - // TODO - return ErrNotImplemented -} - -func (c Connection) StopPod(nameOrID string, timeout int) error { - params := make(map[string]string) - params["t"] = strconv.Itoa(timeout) - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/pods/%s/stop", nameOrID), nil, params) - if err != nil { - return err - } - return response.Process(nil) -} - -func (c Connection) PodTop() error { - // TODO - return ErrNotImplemented // nolint:typecheck -} - -func (c Connection) UnpausePod(nameOrID string) error { - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/pods/%s/unpause", nameOrID), nil, nil) - if err != nil { - return err - } - return response.Process(nil) -} diff --git a/pkg/bindings/pods/pods.go b/pkg/bindings/pods/pods.go new file mode 100644 index 000000000..a6b74c21d --- /dev/null +++ b/pkg/bindings/pods/pods.go @@ -0,0 +1,196 @@ +package pods + +import ( + "context" + "net/http" + "strconv" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/bindings" +) + +func CreatePod() error { + // TODO + return bindings.ErrNotImplemented +} + +// Exists is a lightweight method to determine if a pod exists in local storage +func Exists(ctx context.Context, nameOrID string) (bool, error) { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return false, err + } + response, err := conn.DoRequest(nil, http.MethodGet, "/pods/%s/exists", nil, nameOrID) + if err != nil { + return false, err + } + return response.IsSuccess(), nil +} + +// Inspect returns low-level information about the given pod. +func Inspect(ctx context.Context, nameOrID string) (*libpod.PodInspect, error) { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + inspect := libpod.PodInspect{} + response, err := conn.DoRequest(nil, http.MethodGet, "/pods/%s/json", nil, nameOrID) + if err != nil { + return &inspect, err + } + return &inspect, response.Process(&inspect) +} + +// Kill sends a SIGTERM to all the containers in a pod. The optional signal parameter +// can be used to override SIGTERM. +func Kill(ctx context.Context, nameOrID string, signal *string) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + params := make(map[string]string) + if signal != nil { + params["signal"] = *signal + } + response, err := conn.DoRequest(nil, http.MethodPost, "/pods/%s/kill", params, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +// Pause pauses all running containers in a given pod. +func Pause(ctx context.Context, nameOrID string) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + response, err := conn.DoRequest(nil, http.MethodPost, "/pods/%s/pause", nil, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +// Prune removes all non-running pods in local storage. +func Prune(ctx context.Context) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + response, err := conn.DoRequest(nil, http.MethodPost, "/pods/prune", nil) + if err != nil { + return err + } + return response.Process(nil) +} + +// List returns all pods in local storage. The optional filters parameter can +// be used to refine which pods should be listed. +func List(ctx context.Context, filters map[string][]string) (*[]libpod.PodInspect, error) { + var ( + inspect []libpod.PodInspect + ) + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + params := make(map[string]string) + if filters != nil { + stringFilter, err := bindings.FiltersToHTML(filters) + if err != nil { + return nil, err + } + params["filters"] = stringFilter + } + response, err := conn.DoRequest(nil, http.MethodPost, "/pods/json", params) + if err != nil { + return &inspect, err + } + return &inspect, response.Process(&inspect) +} + +// Restart restarts all containers in a pod. +func Restart(ctx context.Context, nameOrID string) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + response, err := conn.DoRequest(nil, http.MethodPost, "/pods/%s/restart", nil, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +// Remove deletes a Pod from from local storage. The optional force parameter denotes +// that the Pod can be removed even if in a running state. +func Remove(ctx context.Context, nameOrID string, force *bool) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + params := make(map[string]string) + if force != nil { + params["force"] = strconv.FormatBool(*force) + } + response, err := conn.DoRequest(nil, http.MethodDelete, "/pods/%s", params, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +// Start starts all containers in a pod. +func Start(ctx context.Context, nameOrID string) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + response, err := conn.DoRequest(nil, http.MethodDelete, "/pods/%s/start", nil, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +func Stats() error { + // TODO + return bindings.ErrNotImplemented +} + +// Stop stops all containers in a Pod. The optional timeout parameter can be +// used to override the timeout before the container is killed. +func Stop(ctx context.Context, nameOrID string, timeout *int) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + params := make(map[string]string) + if timeout != nil { + params["t"] = strconv.Itoa(*timeout) + } + response, err := conn.DoRequest(nil, http.MethodPost, "/pods/%s/stop", params, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +func Top() error { + // TODO + return bindings.ErrNotImplemented // nolint:typecheck +} + +// Unpause unpauses all paused containers in a Pod. +func Unpause(ctx context.Context, nameOrID string) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + response, err := conn.DoRequest(nil, http.MethodPost, "/pods/%s/unpause", nil, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} diff --git a/pkg/bindings/search.go b/pkg/bindings/search.go deleted file mode 100644 index 0f462357c..000000000 --- a/pkg/bindings/search.go +++ /dev/null @@ -1,39 +0,0 @@ -package bindings - -import ( - "net/http" - "strconv" - - "github.com/containers/libpod/libpod/image" -) - -type ImageSearchFilters struct { - Automated bool `json:"automated"` - Official bool `json:"official"` - Stars int `json:"stars"` -} - -// TODO This method can be concluded when we determine how we want the filters to work on the -// API end -func (i *ImageSearchFilters) ToMapJSON() string { - return "" -} - -func (c Connection) SearchImages(term string, limit int, filters *ImageSearchFilters) ([]image.SearchResult, error) { - var ( - searchResults []image.SearchResult - ) - params := make(map[string]string) - params["term"] = term - if limit > 0 { - params["limit"] = strconv.Itoa(limit) - } - if filters != nil { - params["filters"] = filters.ToMapJSON() - } - response, err := c.newRequest(http.MethodGet, "/images/search", nil, params) - if err != nil { - return searchResults, nil - } - return searchResults, response.Process(&searchResults) -} diff --git a/pkg/bindings/test/common_test.go b/pkg/bindings/test/common_test.go new file mode 100644 index 000000000..4f2a98f2b --- /dev/null +++ b/pkg/bindings/test/common_test.go @@ -0,0 +1,112 @@ +package test_bindings + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/onsi/ginkgo" + "github.com/onsi/gomega/gexec" + "github.com/pkg/errors" +) + +const ( + defaultPodmanBinaryLocation string = "/usr/bin/podman" +) + +type bindingTest struct { + artifactDirPath string + imageCacheDir string + sock string + tempDirPath string + runRoot string + crioRoot string +} + +func (b *bindingTest) runPodman(command []string) *gexec.Session { + var cmd []string + podmanBinary := defaultPodmanBinaryLocation + val, ok := os.LookupEnv("PODMAN_BINARY") + if ok { + podmanBinary = val + } + val, ok = os.LookupEnv("CGROUP_MANAGER") + if ok { + cmd = append(cmd, "--cgroup-manager", val) + } + val, ok = os.LookupEnv("CNI_CONFIG_DIR") + if ok { + cmd = append(cmd, "--cni-config-dir", val) + } + val, ok = os.LookupEnv("CONMON") + if ok { + cmd = append(cmd, "--conmon", val) + } + val, ok = os.LookupEnv("ROOT") + if ok { + cmd = append(cmd, "--root", val) + } else { + cmd = append(cmd, "--root", b.crioRoot) + } + val, ok = os.LookupEnv("OCI_RUNTIME") + if ok { + cmd = append(cmd, "--runtime", val) + } + val, ok = os.LookupEnv("RUNROOT") + if ok { + cmd = append(cmd, "--runroot", val) + } else { + cmd = append(cmd, "--runroot", b.runRoot) + } + val, ok = os.LookupEnv("STORAGE_DRIVER") + if ok { + cmd = append(cmd, "--storage-driver", val) + } + val, ok = os.LookupEnv("STORAGE_OPTIONS") + if ok { + cmd = append(cmd, "--storage", val) + } + cmd = append(cmd, command...) + c := exec.Command(podmanBinary, cmd...) + fmt.Printf("Running: %s %s\n", podmanBinary, strings.Join(cmd, " ")) + session, err := gexec.Start(c, ginkgo.GinkgoWriter, ginkgo.GinkgoWriter) + if err != nil { + panic(errors.Errorf("unable to run podman command: %q", cmd)) + } + return session +} + +func newBindingTest() *bindingTest { + tmpPath, _ := createTempDirInTempDir() + b := bindingTest{ + crioRoot: filepath.Join(tmpPath, "crio"), + runRoot: filepath.Join(tmpPath, "run"), + artifactDirPath: "", + imageCacheDir: "", + sock: fmt.Sprintf("unix:%s", filepath.Join(tmpPath, "api.sock")), + tempDirPath: tmpPath, + } + return &b +} + +// createTempDirinTempDir create a temp dir with prefix podman_test +func createTempDirInTempDir() (string, error) { + return ioutil.TempDir("", "libpod_api") +} + +func (b *bindingTest) startAPIService() *gexec.Session { + var ( + cmd []string + ) + cmd = append(cmd, "--log-level=debug", "service", "--timeout=999999", b.sock) + return b.runPodman(cmd) +} + +func (b *bindingTest) cleanup() { + if err := os.RemoveAll(b.tempDirPath); err != nil { + fmt.Println(err) + } +} diff --git a/pkg/bindings/test/images_test.go b/pkg/bindings/test/images_test.go new file mode 100644 index 000000000..d600197bb --- /dev/null +++ b/pkg/bindings/test/images_test.go @@ -0,0 +1,92 @@ +package test_bindings + +import ( + "context" + "fmt" + "time" + + "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/bindings/images" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Podman images", func() { + var ( + //tempdir string + //err error + //podmanTest *PodmanTestIntegration + bt *bindingTest + s *gexec.Session + connText context.Context + err error + false bool + //true bool = true + ) + + BeforeEach(func() { + //tempdir, err = CreateTempDirInTempDir() + //if err != nil { + // os.Exit(1) + //} + //podmanTest = PodmanTestCreate(tempdir) + //podmanTest.Setup() + //podmanTest.SeedImages() + bt = newBindingTest() + p := bt.runPodman([]string{"pull", "docker.io/library/alpine:latest"}) + p.Wait(45) + s = bt.startAPIService() + time.Sleep(1 * time.Second) + connText, err = bindings.NewConnection(bt.sock) + Expect(err).To(BeNil()) + }) + + AfterEach(func() { + //podmanTest.Cleanup() + //f := CurrentGinkgoTestDescription() + //processTestResult(f) + s.Kill() + bt.cleanup() + }) + It("inspect image", func() { + // Inspect invalid image be 404 + _, err = images.GetImage(connText, "foobar5000", nil) + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", 404)) + + // Inspect by short name + data, err := images.GetImage(connText, "alpine", nil) + Expect(err).To(BeNil()) + + // Inspect with full ID + _, err = images.GetImage(connText, data.ID, nil) + Expect(err).To(BeNil()) + + // Inspect with partial ID + _, err = images.GetImage(connText, data.ID[0:12], nil) + Expect(err).To(BeNil()) + // Inspect by ID + // Inspect by long name should work, it doesnt (yet) i think it needs to be html escaped + //_, err = images.GetImage(connText, ) + //Expect(err).To(BeNil()) + }) + It("remove image", func() { + // Remove invalid image should be a 404 + _, err = images.RemoveImage(connText, "foobar5000", &false) + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", 404)) + + _, err := images.GetImage(connText, "alpine", nil) + Expect(err).To(BeNil()) + + response, err := images.RemoveImage(connText, "alpine", &false) + Expect(err).To(BeNil()) + fmt.Println(response) + // to be continued + + }) + +}) diff --git a/pkg/bindings/volumes.go b/pkg/bindings/volumes.go deleted file mode 100644 index 219f924e7..000000000 --- a/pkg/bindings/volumes.go +++ /dev/null @@ -1,60 +0,0 @@ -package bindings - -import ( - "fmt" - "net/http" - "strconv" - - "github.com/containers/libpod/libpod" - "github.com/containers/libpod/pkg/api/handlers" -) - -func (c Connection) CreateVolume(config handlers.VolumeCreateConfig) (string, error) { - var ( - volumeID string - ) - response, err := c.newRequest(http.MethodPost, "/volumes/create", nil, nil) - if err != nil { - return volumeID, err - } - return volumeID, response.Process(&volumeID) -} - -func (c Connection) InspectVolume(nameOrID string) (*libpod.InspectVolumeData, error) { - var ( - inspect libpod.InspectVolumeData - ) - response, err := c.newRequest(http.MethodPost, fmt.Sprintf("/volumes/%s/json", nameOrID), nil, nil) - if err != nil { - return &inspect, err - } - return &inspect, response.Process(&inspect) -} - -func (c Connection) ListVolumes() error { - // TODO - // The API side of things for this one does a lot in main and therefore - // is not implemented yet. - return ErrNotImplemented // nolint:typecheck -} - -func (c Connection) PruneVolumes() ([]string, error) { - var ( - pruned []string - ) - response, err := c.newRequest(http.MethodPost, "/volumes/prune", nil, nil) - if err != nil { - return pruned, err - } - return pruned, response.Process(&pruned) -} - -func (c Connection) RemoveVolume(nameOrID string, force bool) error { - params := make(map[string]string) - params["force"] = strconv.FormatBool(force) - response, err := c.newRequest(http.MethodPost, "/volumes/prune", nil, params) - if err != nil { - return err - } - return response.Process(nil) -} diff --git a/pkg/bindings/volumes/volumes.go b/pkg/bindings/volumes/volumes.go new file mode 100644 index 000000000..05a4f73fd --- /dev/null +++ b/pkg/bindings/volumes/volumes.go @@ -0,0 +1,85 @@ +package volumes + +import ( + "context" + "net/http" + "strconv" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/bindings" +) + +// Create creates a volume given its configuration. +func Create(ctx context.Context, config handlers.VolumeCreateConfig) (string, error) { + // TODO This is incomplete. The config needs to be sent via the body + var ( + volumeID string + ) + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return "", err + } + response, err := conn.DoRequest(nil, http.MethodPost, "/volumes/create", nil) + if err != nil { + return volumeID, err + } + return volumeID, response.Process(&volumeID) +} + +// Inspect returns low-level information about a volume. +func Inspect(ctx context.Context, nameOrID string) (*libpod.InspectVolumeData, error) { + var ( + inspect libpod.InspectVolumeData + ) + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + response, err := conn.DoRequest(nil, http.MethodPost, "/volumes/%s/json", nil, nameOrID) + if err != nil { + return &inspect, err + } + return &inspect, response.Process(&inspect) +} + +func List() error { + // TODO + // The API side of things for this one does a lot in main and therefore + // is not implemented yet. + return bindings.ErrNotImplemented // nolint:typecheck +} + +// Prune removes unused volumes from the local filesystem. +func Prune(ctx context.Context) ([]string, error) { + var ( + pruned []string + ) + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return nil, err + } + response, err := conn.DoRequest(nil, http.MethodPost, "/volumes/prune", nil) + if err != nil { + return pruned, err + } + return pruned, response.Process(&pruned) +} + +// Remove deletes the given volume from storage. The optional force parameter +// is used to remove a volume even if it is being used by a container. +func Remove(ctx context.Context, nameOrID string, force *bool) error { + conn, err := bindings.GetConnectionFromContext(ctx) + if err != nil { + return err + } + params := make(map[string]string) + if force != nil { + params["force"] = strconv.FormatBool(*force) + } + response, err := conn.DoRequest(nil, http.MethodPost, "/volumes/%s/prune", params, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} diff --git a/pkg/inspect/inspect.go b/pkg/inspect/inspect.go index 8249dc4aa..569f208d9 100644 --- a/pkg/inspect/inspect.go +++ b/pkg/inspect/inspect.go @@ -3,6 +3,7 @@ package inspect import ( "time" + "github.com/containers/image/v5/manifest" "github.com/containers/libpod/libpod/driver" "github.com/opencontainers/go-digest" "github.com/opencontainers/image-spec/specs-go/v1" @@ -10,28 +11,29 @@ import ( // ImageData holds the inspect information of an image type ImageData struct { - ID string `json:"Id"` - Digest digest.Digest `json:"Digest"` - RepoTags []string `json:"RepoTags"` - RepoDigests []string `json:"RepoDigests"` - Parent string `json:"Parent"` - Comment string `json:"Comment"` - Created *time.Time `json:"Created"` - Config *v1.ImageConfig `json:"Config"` - Version string `json:"Version"` - Author string `json:"Author"` - Architecture string `json:"Architecture"` - Os string `json:"Os"` - Size int64 `json:"Size"` - VirtualSize int64 `json:"VirtualSize"` - GraphDriver *driver.Data `json:"GraphDriver"` - RootFS *RootFS `json:"RootFS"` - Labels map[string]string `json:"Labels"` - Annotations map[string]string `json:"Annotations"` - ManifestType string `json:"ManifestType"` - User string `json:"User"` - History []v1.History `json:"History"` - NamesHistory []string `json:"NamesHistory"` + ID string `json:"Id"` + Digest digest.Digest `json:"Digest"` + RepoTags []string `json:"RepoTags"` + RepoDigests []string `json:"RepoDigests"` + Parent string `json:"Parent"` + Comment string `json:"Comment"` + Created *time.Time `json:"Created"` + Config *v1.ImageConfig `json:"Config"` + Version string `json:"Version"` + Author string `json:"Author"` + Architecture string `json:"Architecture"` + Os string `json:"Os"` + Size int64 `json:"Size"` + VirtualSize int64 `json:"VirtualSize"` + GraphDriver *driver.Data `json:"GraphDriver"` + RootFS *RootFS `json:"RootFS"` + Labels map[string]string `json:"Labels"` + Annotations map[string]string `json:"Annotations"` + ManifestType string `json:"ManifestType"` + User string `json:"User"` + History []v1.History `json:"History"` + NamesHistory []string `json:"NamesHistory"` + HealthCheck *manifest.Schema2HealthConfig `json:"Healthcheck,omitempty"` } // RootFS holds the root fs information of an image diff --git a/vendor/github.com/fatih/camelcase/LICENSE.md b/pkg/util/camelcase/LICENSE.md index aa4a536ca..aa4a536ca 100644 --- a/vendor/github.com/fatih/camelcase/LICENSE.md +++ b/pkg/util/camelcase/LICENSE.md diff --git a/vendor/github.com/fatih/camelcase/README.md b/pkg/util/camelcase/README.md index 105a6ae33..105a6ae33 100644 --- a/vendor/github.com/fatih/camelcase/README.md +++ b/pkg/util/camelcase/README.md diff --git a/vendor/github.com/fatih/camelcase/camelcase.go b/pkg/util/camelcase/camelcase.go index 02160c9a4..0a82d1005 100644 --- a/vendor/github.com/fatih/camelcase/camelcase.go +++ b/pkg/util/camelcase/camelcase.go @@ -55,7 +55,7 @@ func Split(src string) (entries []string) { class := 0 // split into fields based on class of unicode character for _, r := range src { - switch true { + switch { case unicode.IsLower(r): class = 1 case unicode.IsUpper(r): @@ -86,5 +86,6 @@ func Split(src string) (entries []string) { entries = append(entries, string(s)) } } - return + + return entries } diff --git a/rootless.md b/rootless.md index d8997a261..93a2b140f 100644 --- a/rootless.md +++ b/rootless.md @@ -44,3 +44,4 @@ can easily fail * If a build is attempting to use a UID that is not mapped into the user namespace mapping for a container, then builds will not be able to put the UID in an image. * Making device nodes within a container fails, even when running --privileged. * The kernel does not allow non root user processes (processes without CAP_MKNOD) to create device nodes. If a container needs to create device nodes, it must be run as root. +* When using --net=host with rootless containers, subsequent podman execs to that container will not join the host network namespace because it is owned by root. diff --git a/test/apiv2/00-TEMPLATE b/test/apiv2/00-TEMPLATE new file mode 100644 index 000000000..e256371ca --- /dev/null +++ b/test/apiv2/00-TEMPLATE @@ -0,0 +1,6 @@ +# -*- sh -*- +# +# FIXME: one-line description of the purpose of this file +# + +# vim: filetype=sh diff --git a/test/apiv2/01-basic.at b/test/apiv2/01-basic.at new file mode 100644 index 000000000..e87ec534c --- /dev/null +++ b/test/apiv2/01-basic.at @@ -0,0 +1,49 @@ +# -*- sh -*- +# +# The earliest most basic tests. If any of these fail, life is bad +# + +# NOTE: paths with a leading slash will be interpreted as-is; +# paths without will have '/v1.40/' prepended. +t GET /_ping 200 OK +t HEAD /_ping 200 +t GET /libpod/_ping 200 OK + +for i in /version version; do + t GET $i 200 \ + .Components[0].Name="Podman Engine" \ + .Components[0].Details.APIVersion=1.40 \ + .Components[0].Details.MinAPIVersion=1.24 \ + .Components[0].Details.Os=linux \ + .ApiVersion=1.40 \ + .MinAPIVersion=1.24 \ + .Os=linux +done + +# +# Garbage tests - requests that should yield errors +# +t GET /nonesuch 404 +t POST /nonesuch '' 404 +t GET container/nonesuch/json 404 +t GET libpod/containers/nonesuch/json 404 +t GET 'libpod/containers/json?a=b' 400 + +# Method not allowed +t POST /_ping '' 405 +t DELETE /_ping 405 +t POST libpod/containers/json '' 405 +t POST libpod/pods/abc '' 405 +t POST info '' 405 +t GET libpod/containers/create 405 + +# +# system info +# +# FIXME: run 'podman info --format=json', and compare select fields +t GET info 200 \ + .OSType=linux \ + .DefaultRuntime=runc \ + .MemTotal~[0-9]\\+ + +# vim: filetype=sh diff --git a/test/apiv2/10-images.at b/test/apiv2/10-images.at new file mode 100644 index 000000000..243b35e9f --- /dev/null +++ b/test/apiv2/10-images.at @@ -0,0 +1,36 @@ +# -*- sh -*- +# +# Tests for image-related endpoints +# + +# FIXME: API doesn't support pull yet, so use podman +podman pull -q $IMAGE + +# We want the SHA without the "sha256:" prefix +full_iid=$(podman images --no-trunc --format '{{.ID}}' $IMAGE) +iid=${full_iid##sha256:} + +t GET libpod/images/$iid/exists 204 +t GET libpod/images/$PODMAN_TEST_IMAGE_NAME/exists 204 + +# FIXME: compare to actual podman info +t GET libpod/images/json 200 \ + .[0].Id=${iid} + +t GET libpod/images/$iid/json 200 \ + .Id=$iid \ + .RepoTags[0]=$IMAGE + +# Same thing, but with abbreviated image id +t GET libpod/images/${iid:0:12}/json 200 \ + .Id=$iid \ + .RepoTags[0]=$IMAGE + +# FIXME: docker API incompatibility: libpod returns 'id', docker 'sha256:id' +t GET images/$iid/json 200 \ + .Id=sha256:$iid \ + .RepoTags[0]=$IMAGE + +#t POST images/create fromImage=alpine 201 foo + +# vim: filetype=sh diff --git a/test/apiv2/20-containers.at b/test/apiv2/20-containers.at new file mode 100644 index 000000000..5f0a145f0 --- /dev/null +++ b/test/apiv2/20-containers.at @@ -0,0 +1,29 @@ +# -*- sh -*- +# +# test container-related endpoints +# + +podman pull $IMAGE &>/dev/null + +# Unimplemented +#t POST libpod/containers/create '' 201 'sdf' + +# Ensure clean slate +podman rm -a -f &>/dev/null + +t GET libpod/containers/json 200 [] + +podman run $IMAGE true + +t GET libpod/containers/json 200 \ + .[0].ID~[0-9a-f]\\{12\\} \ + .[0].Image=$IMAGE \ + .[0].Command=true \ + .[0].State=4 \ + .[0].IsInfra=false + +cid=$(jq -r '.[0].ID' <<<"$output") + +t DELETE libpod/containers/$cid 204 + +# vim: filetype=sh diff --git a/test/apiv2/30-volumes.at b/test/apiv2/30-volumes.at new file mode 100644 index 000000000..b599680e3 --- /dev/null +++ b/test/apiv2/30-volumes.at @@ -0,0 +1,14 @@ +# -*- sh -*- +# +# volume-related tests +# + +# +# FIXME: endpoints seem to be unimplemented, return 404 +# +if false; then +t GET libpod/volumes/json 200 null +t POST libpod/volumes/create name=foo 201 +fi + +# vim: filetype=sh diff --git a/test/apiv2/40-pods.at b/test/apiv2/40-pods.at new file mode 100644 index 000000000..1c25a3822 --- /dev/null +++ b/test/apiv2/40-pods.at @@ -0,0 +1,33 @@ +# -*- sh -*- +# +# test pod-related endpoints +# + +t GET libpod/pods/json 200 null +t POST libpod/pods/create name=foo 201 '{"id":"machine.slice"}' # FIXME! +t GET libpod/pods/foo/exists 204 +t GET libpod/pods/notfoo/exists 404 +t GET libpod/pods/foo/json 200 .Config.name=foo .Containers=null +t GET libpod/pods/json 200 .[0].Config.name=foo .[0].Containers=null + +# Cannot create a dup pod with the same name (FIXME: should that be 409?) +t POST libpod/pods/create name=foo 500 .cause="pod already exists" + +#t POST libpod/pods/create a=b 400 .cause='bad parameter' # FIXME: unimplemented + +t POST libpod/pods/foo/pause '' 204 +t POST libpod/pods/foo/unpause '' 200 +t POST libpod/pods/foo/unpause '' 200 # (2nd time) +t POST libpod/pods/foo/stop '' 304 +t POST libpod/pods/foo/restart '' 500 .cause="no such container" + +t POST libpod/pods/bar/restart '' 404 + +#t POST libpod/pods/prune '' 200 # FIXME: unimplemented, returns 500 +#t POST libpod/pods/prune 'a=b' 400 # FIXME: unimplemented, returns 500 + +# Clean up; and try twice, making sure that the second time fails +t DELETE libpod/pods/foo 204 +t DELETE libpod/pods/foo 404 + +# vim: filetype=sh diff --git a/test/apiv2/README.md b/test/apiv2/README.md new file mode 100644 index 000000000..252d6454e --- /dev/null +++ b/test/apiv2/README.md @@ -0,0 +1,63 @@ +API v2 tests +============ + +This directory contains tests for the podman version 2 API (HTTP). + +Tests themselves are in files of the form 'NN-NAME.at' where NN is a +two-digit number, NAME is a descriptive name, and '.at' is just +an extension I picked. + +Running Tests +============= + +The main test runner is `test-apiv2`. Usage is: + + $ sudo ./test-apiv2 [NAME [...]] + +...where NAME is one or more optional test names, e.g. 'image' or 'pod' +or both. By default, `test-apiv2` will invoke all `*.at` tests. + +`test-apiv2` connects to *localhost only* and *via TCP*. There is +no support here for remote hosts or for UNIX sockets. This is a +framework for testing the API, not all possible protocols. + +`test-apiv2` will start the service if it isn't already running. + + +Writing Tests +============= + +The main test function is `t`. It runs `curl` against the server, +with POST parameters if present, and compares return status and +(optionally) string results from the server: + + t GET /_ping 200 OK + ^^^ ^^^^^^ ^^^ ^^ + | | | +--- expected string result + | | +------- expected return code + | +-------------- endpoint to access + +------------------ method (GET, POST, DELETE, HEAD) + + + t POST libpod/volumes/create name=foo 201 .ID~[0-9a-f]\\{12\\} + ^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^ ^^^ ^^^^^^^^^^^^^^^^^^^^ + | | | JSON '.ID': expect 12-char hex + | | +-- expected code + | +----------- POST params + +--------------------------------- note the missing slash + +Notes: + +* If the endpoint has a leading slash (`/_ping`), `t` leaves it unchanged. +If there's no leading slash, `t` prepends `/v1.40`. This is a simple +convenience for simplicity of writing tests. + +* When method is POST, the argument after the endpoint must be a series +of POST arguments in the form 'key=value', separated by commas. `t` will +convert those to JSON form for passing to the server. + +* The final arguments are one or more expected string results. If an +argument starts with a dot, `t` will invoke `jq` on the output to +fetch that field, and will compare it to the right-hand side of +the argument. If the separator is `=` (equals), `t` will require +an exact match; if `~` (tilde), `t` will use `expr` to compare. diff --git a/test/apiv2/test-apiv2 b/test/apiv2/test-apiv2 new file mode 100755 index 000000000..786c976d6 --- /dev/null +++ b/test/apiv2/test-apiv2 @@ -0,0 +1,325 @@ +#!/bin/bash +# +# Usage: test-apiv2 [PORT] +# +# DEVELOPER NOTE: you almost certainly don't need to play in here. See README. +# +ME=$(basename $0) + +############################################################################### +# BEGIN stuff you can but probably shouldn't customize + +PODMAN_TEST_IMAGE_REGISTRY=${PODMAN_TEST_IMAGE_REGISTRY:-"quay.io"} +PODMAN_TEST_IMAGE_USER=${PODMAN_TEST_IMAGE_USER:-"libpod"} +PODMAN_TEST_IMAGE_NAME=${PODMAN_TEST_IMAGE_NAME:-"alpine_labels"} +PODMAN_TEST_IMAGE_TAG=${PODMAN_TEST_IMAGE_TAG:-"latest"} +PODMAN_TEST_IMAGE_FQN="$PODMAN_TEST_IMAGE_REGISTRY/$PODMAN_TEST_IMAGE_USER/$PODMAN_TEST_IMAGE_NAME:$PODMAN_TEST_IMAGE_TAG" + +IMAGE=$PODMAN_TEST_IMAGE_FQN + +# END stuff you can but probably shouldn't customize +############################################################################### +# BEGIN setup + +TMPDIR=${TMPDIR:-/tmp} +WORKDIR=$(mktemp --tmpdir -d $ME.tmp.XXXXXX) + +# Log of all HTTP requests and responses +LOG=${TMPDIR}/$ME.log.$(date +'%Y%m%dT%H%M%S') + +HOST=localhost +PORT=${PODMAN_SERVICE_PORT:-8081} + +# Keep track of test count and failures in files, not variables, because +# variables don't carry back up from subshells. +testcounter_file=$WORKDIR/.testcounter +failures_file=$WORKDIR/.failures + +echo 0 >$testcounter_file +echo 0 >$failures_file + +# Where the tests live +TESTS_DIR=$(realpath $(dirname $0)) + +# END setup +############################################################################### +# BEGIN infrastructure code - the helper functions used in tests themselves + +######### +# die # Exit error with a message to stderr +######### +function die() { + echo "$ME: $*" >&2 + exit 1 +} + +######## +# is # Simple comparison +######## +function is() { + local actual=$1 + local expect=$2 + local testname=$3 + + if [ "$actual" = "$expect" ]; then + # On success, include expected value; this helps readers understand + _show_ok 1 "$testname=$expect" + return + fi + _show_ok 0 "$testname" "$expect" "$actual" +} + +########## +# like # Compare, but allowing patterns +########## +function like() { + local actual=$1 + local expect=$2 + local testname=$3 + + if expr "$actual" : "$expect" &>/dev/null; then + # On success, include expected value; this helps readers understand + _show_ok 1 "$testname~$expect" + return + fi + _show_ok 0 "$testname" "~ $expect" "$actual" +} + +############## +# _show_ok # Helper for is() and like(): displays 'ok' or 'not ok' +############## +function _show_ok() { + local ok=$1 + local testname=$2 + + # If output is a tty, colorize pass/fail + local red= + local green= + local reset= + local bold= + if [ -t 3 ]; then + red='\e[31m' + green='\e[32m' + reset='\e[0m' + bold='\e[1m' + fi + + _bump $testcounter_file + count=$(<$testcounter_file) + if [ $ok -eq 1 ]; then + echo -e "${green}ok $count $testname${reset}" >&3 + return + fi + + # Failed + local expect=$3 + local actual=$4 + echo -e "${red}not ok $count $testname${reset}" >&3 + echo -e "${red}# expected: $expect${reset}" >&3 + echo -e "${red}# actual: ${bold}$actual${reset}" >&3 + + _bump $failures_file +} + +########### +# _bump # Increment a counter in a file +########### +function _bump() { + local file=$1 + + count=$(<$file) + echo $(( $count + 1 )) >| $file +} + +############# +# jsonify # convert 'foo=bar,x=y' to json {"foo":"bar","x":"y"} +############# +function jsonify() { + # split by comma + local -a settings_in + read -ra settings_in <<<"$1" + + # convert each to double-quoted form + local -a settings_out + for i in ${settings_in[*]}; do + settings_out+=$(sed -e 's/\(.*\)=\(.*\)/"\1":"\2"/' <<<$i) + done + + # ...and wrap inside braces. + # FIXME: handle commas + echo "{${settings_out[*]}}" +} + +####### +# t # Main test helper +####### +function t() { + local method=$1; shift + local path=$1; shift + local curl_args + + local testname="$method $path" + # POST requests require an extra params arg + if [[ $method = "POST" ]]; then + curl_args="-d $(jsonify $1)" + testname="$testname [$1]" + shift + fi + # curl -X HEAD but without --head seems to wait for output anyway + if [[ $method == "HEAD" ]]; then + curl_args="--head" + fi + local expected_code=$1; shift + + # If given path begins with /, use it as-is; otherwise prepend /version/ + local url=http://$HOST:$PORT + if expr "$path" : "/" >/dev/null; then + url="$url$path" + else + url="$url/v1.40/$path" + fi + + # Log every action we do + echo "-------------------------------------------------------------" >>$LOG + echo "\$ $testname" >>$LOG + rm -f $WORKDIR/curl.* + curl -s -X $method ${curl_args} \ + -H 'Content-type: application/json' \ + --dump-header $WORKDIR/curl.headers.out \ + -o $WORKDIR/curl.result.out "$url" + + if [[ $? -eq 7 ]]; then + echo "FATAL: curl failure on $url - cannot continue" >&2 + exit 1 + fi + + cat $WORKDIR/curl.headers.out $WORKDIR/curl.result.out >>$LOG 2>/dev/null || true + + # Test return code + actual_code=$(head -n1 $WORKDIR/curl.headers.out | awk '/^HTTP/ { print $2}') + is "$actual_code" "$expected_code" "$testname : status" + + output=$(< $WORKDIR/curl.result.out) + + for i; do + case "$i" in + # Exact match on json field + .*=*) + json_field=$(expr "$i" : "\([^=]*\)=") + expect=$(expr "$i" : '[^=]*=\(.*\)') + actual=$(jq -r "$json_field" <<<"$output") + is "$actual" "$expect" "$testname : $json_field" + ;; + # regex match on json field + .*~*) + json_field=$(expr "$i" : "\([^~]*\)~") + expect=$(expr "$i" : '[^~]*~\(.*\)') + actual=$(jq -r "$json_field" <<<"$output") + like "$actual" "$expect" "$testname : $json_field" + ;; + # Direct string comparison + *) + is "$output" "$i" "$testname : output" + ;; + esac + done +} + +################### +# start_service # Run the socket listener +################### +service_pid= +function start_service() { + # If there's a listener on the port, nothing for us to do + echo -n >/dev/tcp/$HOST/$PORT &>/dev/null && return + + if [ "$HOST" != "localhost" ]; then + die "Cannot start service on non-localhost ($HOST)" + fi + + if [ $(id -u) -ne 0 ]; then + echo "$ME: WARNING: running service rootless is unlikely to work!" >&2 + fi + + # Find the binary + SERVICE_BIN=${SERVICE_BIN:-${TESTS_DIR}/../../bin/service} + test -x $SERVICE_BIN || die "Not found: $SERVICE_BIN" + + systemd-socket-activate -l 127.0.0.1:$PORT \ + $SERVICE_BIN --root $WORKDIR/root \ + &> $WORKDIR/server.log & + service_pid=$! + + # Wait + local _timeout=5 + while [ $_timeout -gt 0 ]; do + echo -n >/dev/tcp/$HOST/$PORT &>/dev/null && return + sleep 1 + _timeout=$(( $_timeout - 1 )) + done + die "Timed out waiting for service" +} + +# END infrastructure code +############################################################################### +# BEGIN sanity checks + +for tool in curl jq podman; do + type $tool &>/dev/null || die "$ME: Required tool '$tool' not found" +done + +# END sanity checks +############################################################################### +# BEGIN entry handler (subtest invoker) + +# Identify the tests to run. If called with args, use those as globs. +tests_to_run=() +if [ -n "$*" ]; then + shopt -s nullglob + for i; do + match=(${TESTS_DIR}/*${i}*.at) + if [ ${#match} -eq 0 ]; then + die "No match for $TESTS_DIR/*$i*.at" + fi + tests_to_run+=("${match[@]}") + done + shopt -u nullglob +else + tests_to_run=($TESTS_DIR/*.at) +fi + +# Because subtests may run podman or other commands that emit stderr; +# redirect all those and use fd 3 for all output +exec 3>&1 &>$WORKDIR/output.log + +start_service + +for i in ${tests_to_run[@]}; do + source $i +done + +# END entry handler +############################################################################### + +# Clean up + +if [ -n "$service_pid" ]; then + # Yep, has to be -9. It ignores everything else. + kill -9 $service_pid +fi + +test_count=$(<$testcounter_file) +failure_count=$(<$failures_file) + +if [ $failure_count -gt 0 -a -s "$WORKDIR/output.log" ]; then + echo "# Collected stdout/stderr:" >&3 + sed -e 's/^/# /' < $WORKDIR/output.log >&3 +fi + +if [ -z "$PODMAN_TESTS_KEEP_WORKDIR" ]; then + rm -rf $WORKDIR +fi + +echo "1..${test_count}" >&3 + +exit $failure_count diff --git a/test/e2e/inspect_test.go b/test/e2e/inspect_test.go index 9d23384ea..ebac087ac 100644 --- a/test/e2e/inspect_test.go +++ b/test/e2e/inspect_test.go @@ -164,4 +164,17 @@ var _ = Describe("Podman inspect", func() { Expect(inspectDst.ExitCode()).To(Equal(0)) Expect(inspectDst.OutputToString()).To(Equal("/test1")) }) + + It("podman inspect shows healthcheck on docker image", func() { + pull := podmanTest.Podman([]string{"pull", healthcheck}) + pull.WaitWithDefaultTimeout() + Expect(pull.ExitCode()).To(BeZero()) + + session := podmanTest.Podman([]string{"inspect", "--format=json", healthcheck}) + session.WaitWithDefaultTimeout() + imageData := session.InspectImageJSON() + Expect(imageData[0].HealthCheck.Timeout).To(BeNumerically("==", 3000000000)) + Expect(imageData[0].HealthCheck.Interval).To(BeNumerically("==", 60000000000)) + Expect(imageData[0].HealthCheck.Test).To(Equal([]string{"CMD-SHELL", "curl -f http://localhost/ || exit 1"})) + }) }) diff --git a/vendor/github.com/fatih/camelcase/.travis.yml b/vendor/github.com/fatih/camelcase/.travis.yml deleted file mode 100644 index 3489e3871..000000000 --- a/vendor/github.com/fatih/camelcase/.travis.yml +++ /dev/null @@ -1,3 +0,0 @@ -language: go -go: 1.x - diff --git a/vendor/gopkg.in/yaml.v2/scannerc.go b/vendor/gopkg.in/yaml.v2/scannerc.go index b33bdbaec..0b9bb6030 100644 --- a/vendor/gopkg.in/yaml.v2/scannerc.go +++ b/vendor/gopkg.in/yaml.v2/scannerc.go @@ -626,32 +626,18 @@ func trace(args ...interface{}) func() { func yaml_parser_fetch_more_tokens(parser *yaml_parser_t) bool { // While we need more tokens to fetch, do it. for { - // Check if we really need to fetch more tokens. - need_more_tokens := false - - if parser.tokens_head == len(parser.tokens) { - // Queue is empty. - need_more_tokens = true - } else { - // Check if any potential simple key may occupy the head position. - for i := len(parser.simple_keys) - 1; i >= 0; i-- { - simple_key := &parser.simple_keys[i] - if simple_key.token_number < parser.tokens_parsed { - break - } - if valid, ok := yaml_simple_key_is_valid(parser, simple_key); !ok { - return false - } else if valid && simple_key.token_number == parser.tokens_parsed { - need_more_tokens = true - break - } + if parser.tokens_head != len(parser.tokens) { + // If queue is non-empty, check if any potential simple key may + // occupy the head position. + head_tok_idx, ok := parser.simple_keys_by_tok[parser.tokens_parsed] + if !ok { + break + } else if valid, ok := yaml_simple_key_is_valid(parser, &parser.simple_keys[head_tok_idx]); !ok { + return false + } else if !valid { + break } } - - // We are finished. - if !need_more_tokens { - break - } // Fetch the next token. if !yaml_parser_fetch_next_token(parser) { return false @@ -883,6 +869,7 @@ func yaml_parser_save_simple_key(parser *yaml_parser_t) bool { return false } parser.simple_keys[len(parser.simple_keys)-1] = simple_key + parser.simple_keys_by_tok[simple_key.token_number] = len(parser.simple_keys) - 1 } return true } @@ -897,9 +884,10 @@ func yaml_parser_remove_simple_key(parser *yaml_parser_t) bool { "while scanning a simple key", parser.simple_keys[i].mark, "could not find expected ':'") } + // Remove the key from the stack. + parser.simple_keys[i].possible = false + delete(parser.simple_keys_by_tok, parser.simple_keys[i].token_number) } - // Remove the key from the stack. - parser.simple_keys[i].possible = false return true } @@ -930,7 +918,9 @@ func yaml_parser_increase_flow_level(parser *yaml_parser_t) bool { func yaml_parser_decrease_flow_level(parser *yaml_parser_t) bool { if parser.flow_level > 0 { parser.flow_level-- - parser.simple_keys = parser.simple_keys[:len(parser.simple_keys)-1] + last := len(parser.simple_keys) - 1 + delete(parser.simple_keys_by_tok, parser.simple_keys[last].token_number) + parser.simple_keys = parser.simple_keys[:last] } return true } @@ -1007,6 +997,8 @@ func yaml_parser_fetch_stream_start(parser *yaml_parser_t) bool { // Initialize the simple key stack. parser.simple_keys = append(parser.simple_keys, yaml_simple_key_t{}) + parser.simple_keys_by_tok = make(map[int]int) + // A simple key is allowed at the beginning of the stream. parser.simple_key_allowed = true @@ -1310,6 +1302,7 @@ func yaml_parser_fetch_value(parser *yaml_parser_t) bool { // Remove the simple key. simple_key.possible = false + delete(parser.simple_keys_by_tok, simple_key.token_number) // A simple key cannot follow another simple key. parser.simple_key_allowed = false diff --git a/vendor/gopkg.in/yaml.v2/yamlh.go b/vendor/gopkg.in/yaml.v2/yamlh.go index e25cee563..f6a9c8e34 100644 --- a/vendor/gopkg.in/yaml.v2/yamlh.go +++ b/vendor/gopkg.in/yaml.v2/yamlh.go @@ -579,6 +579,7 @@ type yaml_parser_t struct { simple_key_allowed bool // May a simple key occur at the current position? simple_keys []yaml_simple_key_t // The stack of simple keys. + simple_keys_by_tok map[int]int // possible simple_key indexes indexed by token_number // Parser stuff diff --git a/vendor/modules.txt b/vendor/modules.txt index 2aa86270a..df323e0ff 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -261,8 +261,6 @@ github.com/docker/spdystream github.com/docker/spdystream/spdy # github.com/etcd-io/bbolt v1.3.3 github.com/etcd-io/bbolt -# github.com/fatih/camelcase v1.0.0 -github.com/fatih/camelcase # github.com/fsnotify/fsnotify v1.4.7 github.com/fsnotify/fsnotify # github.com/fsouza/go-dockerclient v1.6.0 @@ -446,7 +444,7 @@ github.com/prometheus/common/model github.com/prometheus/procfs github.com/prometheus/procfs/internal/fs github.com/prometheus/procfs/internal/util -# github.com/rootless-containers/rootlesskit v0.7.1 +# github.com/rootless-containers/rootlesskit v0.7.2 github.com/rootless-containers/rootlesskit/pkg/msgutil github.com/rootless-containers/rootlesskit/pkg/port github.com/rootless-containers/rootlesskit/pkg/port/builtin @@ -627,7 +625,7 @@ gopkg.in/square/go-jose.v2/cipher gopkg.in/square/go-jose.v2/json # gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 gopkg.in/tomb.v1 -# gopkg.in/yaml.v2 v2.2.7 +# gopkg.in/yaml.v2 v2.2.8 gopkg.in/yaml.v2 # k8s.io/api v0.17.2 k8s.io/api/core/v1 |