diff options
56 files changed, 1968 insertions, 712 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/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/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-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 753b2ee1c..cddf06e3e 100644 --- a/docs/source/markdown/podman-rm.1.md +++ b/docs/source/markdown/podman-rm.1.md @@ -87,10 +87,13 @@ $ 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 f4d946617..78ef2b157 100644 --- a/docs/source/markdown/podman-rmi.1.md +++ b/docs/source/markdown/podman-rmi.1.md @@ -41,10 +41,13 @@ Remove all images and containers. $ 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.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 diff --git a/install.md b/install.md index 523f3b0f7..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) @@ -100,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 ``` @@ -169,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 ``` @@ -187,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) @@ -199,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 ``` @@ -215,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/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/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/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/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/register_images.go b/pkg/api/server/register_images.go index c59d3d379..f1cc0574c 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -195,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: @@ -607,13 +607,13 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // 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" + // - 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" + // - 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>`) @@ -621,12 +621,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // - `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 + // type: string // produces: // - application/json // responses: @@ -753,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_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/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/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"})) + }) }) |