diff options
Diffstat (limited to 'pkg/bindings')
-rw-r--r-- | pkg/bindings/connection.go | 243 | ||||
-rw-r--r-- | pkg/bindings/containers/containers.go | 69 | ||||
-rw-r--r-- | pkg/bindings/containers/create.go | 30 | ||||
-rw-r--r-- | pkg/bindings/containers/healthcheck.go | 2 | ||||
-rw-r--r-- | pkg/bindings/containers/mount.go | 6 | ||||
-rw-r--r-- | pkg/bindings/errors.go | 2 | ||||
-rw-r--r-- | pkg/bindings/images/images.go | 100 | ||||
-rw-r--r-- | pkg/bindings/images/search.go | 11 | ||||
-rw-r--r-- | pkg/bindings/network/network.go | 6 | ||||
-rw-r--r-- | pkg/bindings/pods/pods.go | 51 | ||||
-rw-r--r-- | pkg/bindings/test/common_test.go | 41 | ||||
-rw-r--r-- | pkg/bindings/test/containers_test.go | 253 | ||||
-rw-r--r-- | pkg/bindings/test/images_test.go | 169 | ||||
-rw-r--r-- | pkg/bindings/test/pods_test.go | 196 | ||||
-rw-r--r-- | pkg/bindings/test/volumes_test.go | 174 | ||||
-rw-r--r-- | pkg/bindings/volumes/volumes.go | 65 |
16 files changed, 1226 insertions, 192 deletions
diff --git a/pkg/bindings/connection.go b/pkg/bindings/connection.go index f270060a6..ba5f9c3aa 100644 --- a/pkg/bindings/connection.go +++ b/pkg/bindings/connection.go @@ -1,22 +1,34 @@ package bindings import ( + "bufio" "context" "fmt" "io" + "io/ioutil" "net" "net/http" "net/url" + "os" "path/filepath" + "strconv" "strings" + "time" "github.com/containers/libpod/pkg/api/handlers" jsoniter "github.com/json-iterator/go" "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "golang.org/x/crypto/ssh" + "k8s.io/client-go/util/homedir" ) var ( - defaultConnectionPath string = filepath.Join(fmt.Sprintf("v%s", handlers.MinimalApiVersion), "libpod") + basePath = &url.URL{ + Scheme: "http", + Host: "d", + Path: "/v" + handlers.MinimalApiVersion + "/libpod", + } ) type APIResponse struct { @@ -25,9 +37,28 @@ type APIResponse struct { } type Connection struct { - scheme string - address string - client *http.Client + _url *url.URL + client *http.Client +} + +type valueKey string + +const ( + clientKey = valueKey("client") +) + +// GetClient from context build by NewConnection() +func GetClient(ctx context.Context) (*Connection, error) { + c, ok := ctx.Value(clientKey).(*Connection) + if !ok { + return nil, errors.Errorf("ClientKey not set in context") + } + return c, nil +} + +// JoinURL elements with '/' +func JoinURL(elements ...string) string { + return strings.Join(elements, "/") } // NewConnection takes a URI as a string and returns a context with the @@ -36,46 +67,81 @@ type Connection struct { // // 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 - } - // TODO once ssh is implemented, remove this block and - // add it to the conditional beneath it - if u.Scheme == "ssh" { - return nil, ErrNotImplemented +// or unix:///run/podman/podman.sock +// or ssh://<user>@<host>[:port]/run/podman/podman.sock?secure=True +func NewConnection(ctx context.Context, uri string, identity ...string) (context.Context, error) { + var ( + err error + secure bool + ) + if v, found := os.LookupEnv("PODMAN_HOST"); found { + uri = v } - if u.Scheme != "tcp" && u.Scheme != "unix" { - return nil, errors.Errorf("%s is not a support schema", u.Scheme) + + if v, found := os.LookupEnv("PODMAN_SSHKEY"); found { + identity = []string{v} } - if u.Scheme == "tcp" && !strings.HasPrefix(uri, "tcp://") { - return nil, errors.New("tcp URIs should begin with tcp://") + _url, err := url.Parse(uri) + if err != nil { + return nil, errors.Wrapf(err, "Value of PODMAN_HOST is not a valid url: %s", uri) } - address := u.Path - if u.Scheme == "tcp" { - address = u.Host + // Now we setup the http client to use the connection above + var client *http.Client + switch _url.Scheme { + case "ssh": + secure, err = strconv.ParseBool(_url.Query().Get("secure")) + if err != nil { + secure = false + } + client, err = sshClient(_url, identity[0], secure) + case "unix": + if !strings.HasPrefix(uri, "unix:///") { + // autofix unix://path_element vs unix:///path_element + _url.Path = JoinURL(_url.Host, _url.Path) + _url.Host = "" + } + client, err = unixClient(_url) + case "tcp": + if !strings.HasPrefix(uri, "tcp://") { + return nil, errors.New("tcp URIs should begin with tcp://") + } + client, err = tcpClient(_url) + default: + return nil, errors.Errorf("%s is not a support schema", _url.Scheme) + } + if err != nil { + return nil, errors.Wrapf(err, "Failed to create %sClient", _url.Scheme) } - newConn := newConnection(u.Scheme, address) - ctx := context.WithValue(context.Background(), "conn", &newConn) + + ctx = context.WithValue(ctx, clientKey, &Connection{_url, client}) if err := pingNewConnection(ctx); err != nil { return nil, err } return ctx, nil } +func tcpClient(_url *url.URL) (*http.Client, error) { + return &http.Client{ + Transport: &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial("tcp", _url.Path) + }, + DisableCompression: true, + }, + }, 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) + client, err := GetClient(ctx) if err != nil { return err } // the ping endpoint sits at / in this case - response, err := conn.DoRequest(nil, http.MethodGet, "../../../_ping", nil) + response, err := client.DoRequest(nil, http.MethodGet, "../../../_ping", nil) if err != nil { return err } @@ -85,30 +151,62 @@ func pingNewConnection(ctx context.Context) error { 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) +func sshClient(_url *url.URL, identity string, secure bool) (*http.Client, error) { + auth, err := publicKey(identity) + if err != nil { + return nil, errors.Wrapf(err, "Failed to parse identity %s: %v\n", _url.String(), identity) + } + + callback := ssh.InsecureIgnoreHostKey() + if secure { + key := hostKey(_url.Hostname()) + if key != nil { + callback = ssh.FixedHostKey(key) + } + } + + bastion, err := ssh.Dial("tcp", + net.JoinHostPort(_url.Hostname(), _url.Port()), + &ssh.ClientConfig{ + User: _url.User.Username(), + Auth: []ssh.AuthMethod{auth}, + HostKeyCallback: callback, + HostKeyAlgorithms: []string{ + ssh.KeyAlgoRSA, + ssh.KeyAlgoDSA, + ssh.KeyAlgoECDSA256, + ssh.KeyAlgoECDSA384, + ssh.KeyAlgoECDSA521, + ssh.KeyAlgoED25519, }, + Timeout: 5 * time.Second, }, + ) + if err != nil { + return nil, errors.Wrapf(err, "Connection to bastion host (%s) failed.", _url.String()) } - newConn := Connection{ - client: &client, - address: address, - scheme: scheme, - } - return newConn + return &http.Client{ + Transport: &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return bastion.Dial("unix", _url.Path) + }, + }}, nil } -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 unixClient(_url *url.URL) (*http.Client, error) { + return &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + d := net.Dialer{} + return d.DialContext(ctx, "unix", _url.Path) + }, + DisableCompression: true, + }, + }, nil } // 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) { +func (c *Connection) DoRequest(httpBody io.Reader, httpMethod, endpoint string, queryParams url.Values, pathValues ...string) (*APIResponse, error) { var ( err error response *http.Response @@ -121,18 +219,13 @@ func (c *Connection) DoRequest(httpBody io.Reader, httpMethod, endpoint string, // Lets eventually use URL for this which might lead to safer // usage safeEndpoint := fmt.Sprintf(endpoint, safePathValues...) - e := c.makeEndpoint(safeEndpoint) + e := basePath.String() + safeEndpoint req, err := http.NewRequest(httpMethod, e, httpBody) if err != nil { return nil, err } 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 queryParams { - r.Add(k, v) - } - req.URL.RawQuery = r.Encode() + req.URL.RawQuery = queryParams.Encode() } // Give the Do three chances in the case of a comm/service hiccup for i := 0; i < 3; i++ { @@ -140,21 +233,11 @@ func (c *Connection) DoRequest(httpBody io.Reader, httpMethod, endpoint string, if err == nil { break } + time.Sleep(time.Duration(i*100) * time.Millisecond) } 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 -} - // FiltersToString converts our typical filter format of a // map[string][]string to a query/html safe string. func FiltersToString(filters map[string][]string) (string, error) { @@ -189,3 +272,45 @@ func (h *APIResponse) IsClientError() bool { func (h *APIResponse) IsServerError() bool { return h.Response.StatusCode/100 == 5 } + +func publicKey(path string) (ssh.AuthMethod, error) { + key, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + signer, err := ssh.ParsePrivateKey(key) + if err != nil { + return nil, err + } + + return ssh.PublicKeys(signer), nil +} + +func hostKey(host string) ssh.PublicKey { + // parse OpenSSH known_hosts file + // ssh or use ssh-keyscan to get initial key + known_hosts := filepath.Join(homedir.HomeDir(), ".ssh", "known_hosts") + fd, err := os.Open(known_hosts) + if err != nil { + logrus.Error(err) + return nil + } + + scanner := bufio.NewScanner(fd) + for scanner.Scan() { + _, hosts, key, _, _, err := ssh.ParseKnownHosts(scanner.Bytes()) + if err != nil { + logrus.Errorf("Failed to parse known_hosts: %s", scanner.Text()) + continue + } + + for _, h := range hosts { + if h == host { + return key + } + } + } + + return nil +} diff --git a/pkg/bindings/containers/containers.go b/pkg/bindings/containers/containers.go index 04f7f8802..2985787a6 100644 --- a/pkg/bindings/containers/containers.go +++ b/pkg/bindings/containers/containers.go @@ -3,6 +3,7 @@ package containers import ( "context" "net/http" + "net/url" "strconv" "github.com/containers/libpod/libpod" @@ -16,33 +17,33 @@ import ( // 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, all *bool, last *int, pod, size, sync *bool) ([]lpapiv2.ListContainer, error) { // nolint:typecheck - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } var containers []lpapiv2.ListContainer - params := make(map[string]string) + params := url.Values{} if all != nil { - params["all"] = strconv.FormatBool(*all) + params.Set("all", strconv.FormatBool(*all)) } if last != nil { - params["last"] = strconv.Itoa(*last) + params.Set("last", strconv.Itoa(*last)) } if pod != nil { - params["pod"] = strconv.FormatBool(*pod) + params.Set("pod", strconv.FormatBool(*pod)) } if size != nil { - params["size"] = strconv.FormatBool(*size) + params.Set("size", strconv.FormatBool(*size)) } if sync != nil { - params["sync"] = strconv.FormatBool(*sync) + params.Set("sync", strconv.FormatBool(*sync)) } if filters != nil { filterString, err := bindings.FiltersToString(filters) if err != nil { return nil, err } - params["filters"] = filterString + params.Set("filters", filterString) } response, err := conn.DoRequest(nil, http.MethodGet, "/containers/json", params) if err != nil { @@ -59,17 +60,17 @@ func Prune(ctx context.Context, filters map[string][]string) ([]string, error) { var ( pruneResponse []string ) - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } - params := make(map[string]string) + params := url.Values{} if filters != nil { filterString, err := bindings.FiltersToString(filters) if err != nil { return nil, err } - params["filters"] = filterString + params.Set("filters", filterString) } response, err := conn.DoRequest(nil, http.MethodPost, "/containers/prune", params) if err != nil { @@ -82,16 +83,16 @@ func Prune(ctx context.Context, filters map[string][]string) ([]string, error) { // 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) + conn, err := bindings.GetClient(ctx) if err != nil { return err } - params := make(map[string]string) + params := url.Values{} if force != nil { - params["force"] = strconv.FormatBool(*force) + params.Set("force", strconv.FormatBool(*force)) } if volumes != nil { - params["vols"] = strconv.FormatBool(*volumes) + params.Set("vols", strconv.FormatBool(*volumes)) } response, err := conn.DoRequest(nil, http.MethodDelete, "/containers/%s", params, nameOrID) if err != nil { @@ -105,13 +106,13 @@ func Remove(ctx context.Context, nameOrID string, force, volumes *bool) error { // 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) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } - params := make(map[string]string) + params := url.Values{} if size != nil { - params["size"] = strconv.FormatBool(*size) + params.Set("size", strconv.FormatBool(*size)) } response, err := conn.DoRequest(nil, http.MethodGet, "/containers/%s/json", params, nameOrID) if err != nil { @@ -125,12 +126,12 @@ func Inspect(ctx context.Context, nameOrID string, size *bool) (*libpod.InspectC // 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) + conn, err := bindings.GetClient(ctx) if err != nil { return err } - params := make(map[string]string) - params["signal"] = signal + params := url.Values{} + params.Set("signal", signal) response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/kill", params, nameOrID) if err != nil { return err @@ -143,7 +144,7 @@ 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) + conn, err := bindings.GetClient(ctx) if err != nil { return err } @@ -158,13 +159,13 @@ func Pause(ctx context.Context, nameOrID string) error { // 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) + conn, err := bindings.GetClient(ctx) if err != nil { return err } - params := make(map[string]string) + params := url.Values{} if timeout != nil { - params["t"] = strconv.Itoa(*timeout) + params.Set("t", strconv.Itoa(*timeout)) } response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/restart", params, nameOrID) if err != nil { @@ -177,13 +178,13 @@ func Restart(ctx context.Context, nameOrID string, timeout *int) error { // 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) + conn, err := bindings.GetClient(ctx) if err != nil { return err } - params := make(map[string]string) + params := url.Values{} if detachKeys != nil { - params["detachKeys"] = *detachKeys + params.Set("detachKeys", *detachKeys) } response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/start", params, nameOrID) if err != nil { @@ -198,7 +199,7 @@ 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) + conn, err := bindings.GetClient(ctx) if err != nil { return err } @@ -213,7 +214,7 @@ func Unpause(ctx context.Context, nameOrID string) error { // or a partial/full ID. func Wait(ctx context.Context, nameOrID string) (int32, error) { var exitCode int32 - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return exitCode, err } @@ -228,7 +229,7 @@ func Wait(ctx context.Context, nameOrID string) (int32, error) { // 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) + conn, err := bindings.GetClient(ctx) if err != nil { return false, err } @@ -242,13 +243,13 @@ func Exists(ctx context.Context, nameOrID string) (bool, error) { // 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) + params := url.Values{} + conn, err := bindings.GetClient(ctx) if err != nil { return err } if timeout != nil { - params["t"] = strconv.Itoa(*timeout) + params.Set("t", strconv.Itoa(*timeout)) } response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/stop", params, nameOrID) if err != nil { diff --git a/pkg/bindings/containers/create.go b/pkg/bindings/containers/create.go new file mode 100644 index 000000000..43a3ef02d --- /dev/null +++ b/pkg/bindings/containers/create.go @@ -0,0 +1,30 @@ +package containers + +import ( + "context" + "net/http" + "strings" + + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/specgen" + jsoniter "github.com/json-iterator/go" +) + +func CreateWithSpec(ctx context.Context, s specgen.SpecGenerator) (utils.ContainerCreateResponse, error) { + var ccr utils.ContainerCreateResponse + conn, err := bindings.GetClient(ctx) + if err != nil { + return ccr, err + } + specgenString, err := jsoniter.MarshalToString(s) + if err != nil { + return ccr, err + } + stringReader := strings.NewReader(specgenString) + response, err := conn.DoRequest(stringReader, http.MethodPost, "/containers/create", nil) + if err != nil { + return ccr, err + } + return ccr, response.Process(&ccr) +} diff --git a/pkg/bindings/containers/healthcheck.go b/pkg/bindings/containers/healthcheck.go index 9ed7f858d..dc607c1b3 100644 --- a/pkg/bindings/containers/healthcheck.go +++ b/pkg/bindings/containers/healthcheck.go @@ -11,7 +11,7 @@ import ( // 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) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } diff --git a/pkg/bindings/containers/mount.go b/pkg/bindings/containers/mount.go index d68dee981..e0627d9a3 100644 --- a/pkg/bindings/containers/mount.go +++ b/pkg/bindings/containers/mount.go @@ -10,7 +10,7 @@ import ( // 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) + conn, err := bindings.GetClient(ctx) if err != nil { return "", err } @@ -27,7 +27,7 @@ func Mount(ctx context.Context, nameOrID string) (string, error) { // 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) + conn, err := bindings.GetClient(ctx) if err != nil { return err } @@ -40,7 +40,7 @@ func Unmount(ctx context.Context, nameOrID string) error { // 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) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } diff --git a/pkg/bindings/errors.go b/pkg/bindings/errors.go index 1bcaac3f0..5fa711199 100644 --- a/pkg/bindings/errors.go +++ b/pkg/bindings/errors.go @@ -25,7 +25,7 @@ func (a APIResponse) Process(unmarshalInto interface{}) error { if err != nil { return errors.Wrap(err, "unable to process API response") } - if a.IsSuccess() { + if a.IsSuccess() || a.IsRedirection() { if unmarshalInto != nil { return json.Unmarshal(data, unmarshalInto) } diff --git a/pkg/bindings/images/images.go b/pkg/bindings/images/images.go index b19482943..c84aa4601 100644 --- a/pkg/bindings/images/images.go +++ b/pkg/bindings/images/images.go @@ -2,8 +2,10 @@ package images import ( "context" + "errors" "io" "net/http" + "net/url" "strconv" "github.com/containers/libpod/pkg/api/handlers" @@ -14,7 +16,7 @@ import ( // 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) + conn, err := bindings.GetClient(ctx) if err != nil { return false, err } @@ -29,20 +31,20 @@ func Exists(ctx context.Context, nameOrID string) (bool, error) { // 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) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } - params := make(map[string]string) + params := url.Values{} if all != nil { - params["all"] = strconv.FormatBool(*all) + params.Set("all", strconv.FormatBool(*all)) } if filters != nil { strFilters, err := bindings.FiltersToString(filters) if err != nil { return nil, err } - params["filters"] = strFilters + params.Set("filters", strFilters) } response, err := conn.DoRequest(nil, http.MethodGet, "/images/json", params) if err != nil { @@ -54,13 +56,13 @@ func List(ctx context.Context, all *bool, filters map[string][]string) ([]*handl // 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) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } - params := make(map[string]string) + params := url.Values{} if size != nil { - params["size"] = strconv.FormatBool(*size) + params.Set("size", strconv.FormatBool(*size)) } inspectedData := inspect.ImageData{} response, err := conn.DoRequest(nil, http.MethodGet, "/images/%s/json", params, nameOrID) @@ -77,7 +79,7 @@ func ImageTree(ctx context.Context, nameOrId string) error { // 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) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } @@ -88,28 +90,34 @@ func History(ctx context.Context, nameOrID string) ([]*handlers.HistoryResponse, return history, response.Process(&history) } -func Load(ctx context.Context, r io.Reader) error { - conn, err := bindings.GetConnectionFromContext(ctx) +func Load(ctx context.Context, r io.Reader, name *string) (string, error) { + var id handlers.IDResponse + conn, err := bindings.GetClient(ctx) if err != nil { - return err + return "", err } - // TODO this still needs error handling added - //_, err := http.Post(c.makeEndpoint("/images/loads"), "application/json", r) //nolint - _ = conn - return bindings.ErrNotImplemented + params := url.Values{} + if name != nil { + params.Set("reference", *name) + } + response, err := conn.DoRequest(r, http.MethodPost, "/images/load", params) + if err != nil { + return "", err + } + return id.ID, response.Process(&id) } // 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) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } - params := make(map[string]string) + params := url.Values{} if force != nil { - params["force"] = strconv.FormatBool(*force) + params.Set("force", strconv.FormatBool(*force)) } response, err := conn.DoRequest(nil, http.MethodDelete, "/images/%s", params, nameOrID) if err != nil { @@ -121,16 +129,16 @@ func Remove(ctx context.Context, nameOrID string, force *bool) ([]map[string]str // 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) + conn, err := bindings.GetClient(ctx) if err != nil { return err } - params := make(map[string]string) + params := url.Values{} if format != nil { - params["format"] = *format + params.Set("format", *format) } if compress != nil { - params["compress"] = strconv.FormatBool(*compress) + params.Set("compress", strconv.FormatBool(*compress)) } response, err := conn.DoRequest(nil, http.MethodGet, "/images/%s/get", params, nameOrID) if err != nil { @@ -149,17 +157,17 @@ func Prune(ctx context.Context, filters map[string][]string) ([]string, error) { var ( deleted []string ) - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } - params := make(map[string]string) + params := url.Values{} if filters != nil { stringFilter, err := bindings.FiltersToString(filters) if err != nil { return nil, err } - params["filters"] = stringFilter + params.Set("filters", stringFilter) } response, err := conn.DoRequest(nil, http.MethodPost, "/images/prune", params) if err != nil { @@ -170,13 +178,13 @@ func Prune(ctx context.Context, filters map[string][]string) ([]string, error) { // 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) + conn, err := bindings.GetClient(ctx) if err != nil { return err } - params := make(map[string]string) - params["tag"] = tag - params["repo"] = repo + params := url.Values{} + params.Set("tag", tag) + params.Set("repo", repo) response, err := conn.DoRequest(nil, http.MethodPost, "/images/%s/tag", params, nameOrID) if err != nil { return err @@ -185,3 +193,35 @@ func Tag(ctx context.Context, nameOrID, tag, repo string) error { } func Build(nameOrId string) {} + +// Imports adds the given image to the local image store. This can be done by file and the given reader +// or via the url parameter. Additional metadata can be associated with the image by using the changes and +// message parameters. The image can also be tagged given a reference. One of url OR r must be provided. +func Import(ctx context.Context, changes []string, message, reference, u *string, r io.Reader) (string, error) { + var id handlers.IDResponse + if r != nil && u != nil { + return "", errors.New("url and r parameters cannot be used together") + } + conn, err := bindings.GetClient(ctx) + if err != nil { + return "", err + } + params := url.Values{} + for _, change := range changes { + params.Add("changes", change) + } + if message != nil { + params.Set("message", *message) + } + if reference != nil { + params.Set("reference", *reference) + } + if u != nil { + params.Set("url", *u) + } + response, err := conn.DoRequest(r, http.MethodPost, "/images/import", params) + if err != nil { + return "", err + } + return id.ID, response.Process(&id) +} diff --git a/pkg/bindings/images/search.go b/pkg/bindings/images/search.go index 58b25425b..183ff3d77 100644 --- a/pkg/bindings/images/search.go +++ b/pkg/bindings/images/search.go @@ -3,6 +3,7 @@ package images import ( "context" "net/http" + "net/url" "strconv" "github.com/containers/libpod/libpod/image" @@ -16,21 +17,21 @@ func Search(ctx context.Context, term string, limit *int, filters map[string][]s var ( searchResults []image.SearchResult ) - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } - params := make(map[string]string) - params["term"] = term + params := url.Values{} + params.Set("term", term) if limit != nil { - params["limit"] = strconv.Itoa(*limit) + params.Set("limit", strconv.Itoa(*limit)) } if filters != nil { stringFilter, err := bindings.FiltersToString(filters) if err != nil { return nil, err } - params["filters"] = stringFilter + params.Set("filters", stringFilter) } response, err := conn.DoRequest(nil, http.MethodGet, "/images/search", params) if err != nil { diff --git a/pkg/bindings/network/network.go b/pkg/bindings/network/network.go index 97bbb8c42..c95b22953 100644 --- a/pkg/bindings/network/network.go +++ b/pkg/bindings/network/network.go @@ -10,7 +10,7 @@ import ( func Create() {} func Inspect(ctx context.Context, nameOrID string) (map[string]interface{}, error) { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } @@ -23,7 +23,7 @@ func Inspect(ctx context.Context, nameOrID string) (map[string]interface{}, erro } func Remove(ctx context.Context, nameOrID string) error { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } @@ -38,7 +38,7 @@ func List(ctx context.Context) ([]*libcni.NetworkConfigList, error) { var ( netList []*libcni.NetworkConfigList ) - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } diff --git a/pkg/bindings/pods/pods.go b/pkg/bindings/pods/pods.go index d079f01c2..1a8c31be1 100644 --- a/pkg/bindings/pods/pods.go +++ b/pkg/bindings/pods/pods.go @@ -3,6 +3,7 @@ package pods import ( "context" "net/http" + "net/url" "strconv" "github.com/containers/libpod/libpod" @@ -16,7 +17,7 @@ func CreatePod() error { // 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) + conn, err := bindings.GetClient(ctx) if err != nil { return false, err } @@ -29,7 +30,7 @@ func Exists(ctx context.Context, nameOrID string) (bool, error) { // Inspect returns low-level information about the given pod. func Inspect(ctx context.Context, nameOrID string) (*libpod.PodInspect, error) { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } @@ -44,13 +45,13 @@ func Inspect(ctx context.Context, nameOrID string) (*libpod.PodInspect, error) { // 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) + conn, err := bindings.GetClient(ctx) if err != nil { return err } - params := make(map[string]string) + params := url.Values{} if signal != nil { - params["signal"] = *signal + params.Set("signal", *signal) } response, err := conn.DoRequest(nil, http.MethodPost, "/pods/%s/kill", params, nameOrID) if err != nil { @@ -61,7 +62,7 @@ func Kill(ctx context.Context, nameOrID string, signal *string) error { // Pause pauses all running containers in a given pod. func Pause(ctx context.Context, nameOrID string) error { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } @@ -74,7 +75,7 @@ func Pause(ctx context.Context, nameOrID string) error { // Prune removes all non-running pods in local storage. func Prune(ctx context.Context) error { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } @@ -87,32 +88,32 @@ func Prune(ctx context.Context) error { // 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) { +func List(ctx context.Context, filters map[string][]string) ([]*libpod.PodInspect, error) { var ( - inspect []libpod.PodInspect + inspect []*libpod.PodInspect ) - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } - params := make(map[string]string) + params := url.Values{} if filters != nil { stringFilter, err := bindings.FiltersToString(filters) if err != nil { return nil, err } - params["filters"] = stringFilter + params.Set("filters", stringFilter) } - response, err := conn.DoRequest(nil, http.MethodPost, "/pods/json", params) + response, err := conn.DoRequest(nil, http.MethodGet, "/pods/json", params) if err != nil { - return &inspect, err + return inspect, err } - return &inspect, response.Process(&inspect) + 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) + conn, err := bindings.GetClient(ctx) if err != nil { return err } @@ -126,13 +127,13 @@ func Restart(ctx context.Context, nameOrID string) error { // 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) + conn, err := bindings.GetClient(ctx) if err != nil { return err } - params := make(map[string]string) + params := url.Values{} if force != nil { - params["force"] = strconv.FormatBool(*force) + params.Set("force", strconv.FormatBool(*force)) } response, err := conn.DoRequest(nil, http.MethodDelete, "/pods/%s", params, nameOrID) if err != nil { @@ -143,11 +144,11 @@ func Remove(ctx context.Context, nameOrID string, force *bool) error { // Start starts all containers in a pod. func Start(ctx context.Context, nameOrID string) error { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } - response, err := conn.DoRequest(nil, http.MethodDelete, "/pods/%s/start", nil, nameOrID) + response, err := conn.DoRequest(nil, http.MethodPost, "/pods/%s/start", nil, nameOrID) if err != nil { return err } @@ -162,13 +163,13 @@ func Stats() error { // 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) + conn, err := bindings.GetClient(ctx) if err != nil { return err } - params := make(map[string]string) + params := url.Values{} if timeout != nil { - params["t"] = strconv.Itoa(*timeout) + params.Set("t", strconv.Itoa(*timeout)) } response, err := conn.DoRequest(nil, http.MethodPost, "/pods/%s/stop", params, nameOrID) if err != nil { @@ -184,7 +185,7 @@ func Top() error { // Unpause unpauses all paused containers in a Pod. func Unpause(ctx context.Context, nameOrID string) error { - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return err } diff --git a/pkg/bindings/test/common_test.go b/pkg/bindings/test/common_test.go index dba94cb35..38f5014ca 100644 --- a/pkg/bindings/test/common_test.go +++ b/pkg/bindings/test/common_test.go @@ -20,9 +20,18 @@ type testImage struct { } const ( + devPodmanBinaryLocation string = "../../../bin/podman" defaultPodmanBinaryLocation string = "/usr/bin/podman" ) +func getPodmanBinary() string { + _, err := os.Stat(devPodmanBinaryLocation) + if os.IsNotExist(err) { + return defaultPodmanBinaryLocation + } + return devPodmanBinaryLocation +} + var ( ImageCacheDir = "/tmp/podman/imagecachedir" LockTmpDir string @@ -50,7 +59,7 @@ type bindingTest struct { func (b *bindingTest) runPodman(command []string) *gexec.Session { var cmd []string - podmanBinary := defaultPodmanBinaryLocation + podmanBinary := getPodmanBinary() val, ok := os.LookupEnv("PODMAN_BINARY") if ok { podmanBinary = val @@ -114,7 +123,7 @@ func newBindingTest() *bindingTest { runRoot: filepath.Join(tmpPath, "run"), artifactDirPath: "", imageCacheDir: "", - sock: fmt.Sprintf("unix:%s", filepath.Join(tmpPath, "api.sock")), + sock: fmt.Sprintf("unix://%s", filepath.Join(tmpPath, "api.sock")), tempDirPath: tmpPath, } return &b @@ -162,16 +171,30 @@ func (b *bindingTest) restoreImageFromCache(i testImage) { p.Wait(45) } -// Run a container and add append the alpine image to it -func (b *bindingTest) RunTopContainer(name *string) { +// Run a container within or without a pod +// and add or append the alpine image to it +func (b *bindingTest) RunTopContainer(containerName *string, insidePod *bool, podName *string) { cmd := []string{"run", "-dt"} - if name != nil { - containerName := *name - cmd = append(cmd, "--name", containerName) + if insidePod != nil && podName != nil { + pName := *podName + cmd = append(cmd, "--pod", pName) + } else if containerName != nil { + cName := *containerName + cmd = append(cmd, "--name", cName) } cmd = append(cmd, alpine.name, "top") - p := b.runPodman(cmd) - p.Wait(45) + b.runPodman(cmd).Wait(45) +} + +// This method creates a pod with the given pod name. +// Podname is an optional parameter +func (b *bindingTest) Podcreate(name *string) { + if name != nil { + podname := *name + b.runPodman([]string{"pod", "create", "--name", podname}).Wait(45) + } else { + b.runPodman([]string{"pod", "create"}).Wait(45) + } } // StringInSlice returns a boolean based on whether a given diff --git a/pkg/bindings/test/containers_test.go b/pkg/bindings/test/containers_test.go new file mode 100644 index 000000000..6756e81c7 --- /dev/null +++ b/pkg/bindings/test/containers_test.go @@ -0,0 +1,253 @@ +package test_bindings + +import ( + "context" + "net/http" + "strconv" + "time" + + "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/bindings/containers" + "github.com/containers/libpod/test/utils" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Podman containers ", func() { + var ( + bt *bindingTest + s *gexec.Session + connText context.Context + err error + falseFlag bool = false + trueFlag bool = true + ) + + BeforeEach(func() { + bt = newBindingTest() + bt.RestoreImagesFromCache() + s = bt.startAPIService() + time.Sleep(1 * time.Second) + connText, err = bindings.NewConnection(context.Background(), bt.sock) + Expect(err).To(BeNil()) + }) + + AfterEach(func() { + s.Kill() + bt.cleanup() + }) + + It("podman pause a bogus container", func() { + // Pausing bogus container should return 404 + err = containers.Pause(connText, "foobar") + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusNotFound)) + }) + + It("podman unpause a bogus container", func() { + // Unpausing bogus container should return 404 + err = containers.Unpause(connText, "foobar") + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusNotFound)) + }) + + It("podman pause a running container by name", func() { + // Pausing by name should work + var name = "top" + bt.RunTopContainer(&name, &falseFlag, nil) + err := containers.Pause(connText, name) + Expect(err).To(BeNil()) + + // Ensure container is paused + data, err := containers.Inspect(connText, name, nil) + Expect(err).To(BeNil()) + Expect(data.State.Status).To(Equal("paused")) + }) + + It("podman pause a running container by id", func() { + // Pausing by id should work + var name = "top" + bt.RunTopContainer(&name, &falseFlag, nil) + data, err := containers.Inspect(connText, name, nil) + Expect(err).To(BeNil()) + err = containers.Pause(connText, data.ID) + Expect(err).To(BeNil()) + + // Ensure container is paused + data, err = containers.Inspect(connText, data.ID, nil) + Expect(data.State.Status).To(Equal("paused")) + }) + + It("podman unpause a running container by name", func() { + // Unpausing by name should work + var name = "top" + bt.RunTopContainer(&name, &falseFlag, nil) + err := containers.Pause(connText, name) + Expect(err).To(BeNil()) + err = containers.Unpause(connText, name) + Expect(err).To(BeNil()) + + // Ensure container is unpaused + data, err := containers.Inspect(connText, name, nil) + Expect(data.State.Status).To(Equal("running")) + }) + + It("podman unpause a running container by ID", func() { + // Unpausing by ID should work + var name = "top" + bt.RunTopContainer(&name, &falseFlag, nil) + // Pause by name + err := containers.Pause(connText, name) + data, err := containers.Inspect(connText, name, nil) + Expect(err).To(BeNil()) + err = containers.Unpause(connText, data.ID) + Expect(err).To(BeNil()) + + // Ensure container is unpaused + data, err = containers.Inspect(connText, name, nil) + Expect(data.State.Status).To(Equal("running")) + }) + + It("podman pause a paused container by name", func() { + // Pausing a paused container by name should fail + var name = "top" + bt.RunTopContainer(&name, &falseFlag, nil) + err := containers.Pause(connText, name) + Expect(err).To(BeNil()) + err = containers.Pause(connText, name) + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) + }) + + It("podman pause a paused container by id", func() { + // Pausing a paused container by id should fail + var name = "top" + bt.RunTopContainer(&name, &falseFlag, nil) + data, err := containers.Inspect(connText, name, nil) + Expect(err).To(BeNil()) + err = containers.Pause(connText, data.ID) + Expect(err).To(BeNil()) + err = containers.Pause(connText, data.ID) + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) + }) + + It("podman pause a stopped container by name", func() { + // Pausing a stopped container by name should fail + var name = "top" + bt.RunTopContainer(&name, &falseFlag, nil) + err := containers.Stop(connText, name, nil) + Expect(err).To(BeNil()) + err = containers.Pause(connText, name) + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) + }) + + It("podman pause a stopped container by id", func() { + // Pausing a stopped container by id should fail + var name = "top" + bt.RunTopContainer(&name, &falseFlag, nil) + data, err := containers.Inspect(connText, name, nil) + err = containers.Stop(connText, data.ID, nil) + Expect(err).To(BeNil()) + err = containers.Pause(connText, data.ID) + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) + }) + + It("podman remove a paused container by id without force", func() { + // Removing a paused container without force should fail + var name = "top" + bt.RunTopContainer(&name, &falseFlag, nil) + data, err := containers.Inspect(connText, name, nil) + Expect(err).To(BeNil()) + err = containers.Pause(connText, data.ID) + Expect(err).To(BeNil()) + err = containers.Remove(connText, data.ID, &falseFlag, &falseFlag) + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) + }) + + It("podman remove a paused container by id with force", func() { + // FIXME: Skip on F31 and later + host := utils.GetHostDistributionInfo() + osVer, err := strconv.Atoi(host.Version) + Expect(err).To(BeNil()) + if host.Distribution == "fedora" && osVer >= 31 { + Skip("FIXME: https://github.com/containers/libpod/issues/5325") + } + + // Removing a paused container with force should work + var name = "top" + bt.RunTopContainer(&name, &falseFlag, nil) + data, err := containers.Inspect(connText, name, nil) + Expect(err).To(BeNil()) + err = containers.Pause(connText, data.ID) + Expect(err).To(BeNil()) + err = containers.Remove(connText, data.ID, &trueFlag, &falseFlag) + Expect(err).To(BeNil()) + }) + + It("podman stop a paused container by name", func() { + // Stopping a paused container by name should fail + var name = "top" + bt.RunTopContainer(&name, &falseFlag, nil) + err := containers.Pause(connText, name) + Expect(err).To(BeNil()) + err = containers.Stop(connText, name, nil) + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) + }) + + It("podman stop a paused container by id", func() { + // Stopping a paused container by id should fail + var name = "top" + bt.RunTopContainer(&name, &falseFlag, nil) + data, err := containers.Inspect(connText, name, nil) + Expect(err).To(BeNil()) + err = containers.Pause(connText, data.ID) + Expect(err).To(BeNil()) + err = containers.Stop(connText, data.ID, nil) + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) + }) + + It("podman stop a running container by name", func() { + // Stopping a running container by name should work + var name = "top" + bt.RunTopContainer(&name, &falseFlag, nil) + err := containers.Stop(connText, name, nil) + Expect(err).To(BeNil()) + + // Ensure container is stopped + data, err := containers.Inspect(connText, name, nil) + Expect(err).To(BeNil()) + Expect(data.State.Status).To(Equal("exited")) + }) + + It("podman stop a running container by ID", func() { + // Stopping a running container by ID should work + var name = "top" + bt.RunTopContainer(&name, &falseFlag, nil) + data, err := containers.Inspect(connText, name, nil) + Expect(err).To(BeNil()) + err = containers.Stop(connText, data.ID, nil) + Expect(err).To(BeNil()) + + // Ensure container is stopped + data, err = containers.Inspect(connText, name, nil) + Expect(err).To(BeNil()) + Expect(data.State.Status).To(Equal("exited")) + }) + +}) diff --git a/pkg/bindings/test/images_test.go b/pkg/bindings/test/images_test.go index 74e0cc67a..8eef28502 100644 --- a/pkg/bindings/test/images_test.go +++ b/pkg/bindings/test/images_test.go @@ -3,6 +3,8 @@ package test_bindings import ( "context" "net/http" + "os" + "path/filepath" "time" "github.com/containers/libpod/pkg/bindings" @@ -38,7 +40,7 @@ var _ = Describe("Podman images", func() { bt.RestoreImagesFromCache() s = bt.startAPIService() time.Sleep(1 * time.Second) - connText, err = bindings.NewConnection(bt.sock) + connText, err = bindings.NewConnection(context.Background(), bt.sock) Expect(err).To(BeNil()) }) @@ -71,6 +73,14 @@ var _ = Describe("Podman images", func() { // Inspect by long name _, err = images.GetImage(connText, alpine.name, nil) Expect(err).To(BeNil()) + // TODO it looks like the images API alwaays returns size regardless + // of bool or not. What should we do ? + //Expect(data.Size).To(BeZero()) + + // Enabling the size parameter should result in size being populated + data, err = images.GetImage(connText, alpine.name, &trueFlag) + Expect(err).To(BeNil()) + Expect(data.Size).To(BeNumerically(">", 0)) }) // Test to validate the remove image api @@ -93,7 +103,7 @@ var _ = Describe("Podman images", func() { // Start a container with alpine image var top string = "top" - bt.RunTopContainer(&top) + bt.RunTopContainer(&top, &falseFlag, nil) // we should now have a container called "top" running containerResponse, err := containers.Inspect(connText, "top", &falseFlag) Expect(err).To(BeNil()) @@ -181,4 +191,159 @@ var _ = Describe("Podman images", func() { Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) }) + It("Image Exists", func() { + // exists on bogus image should be false, with no error + exists, err := images.Exists(connText, "foobar") + Expect(err).To(BeNil()) + Expect(exists).To(BeFalse()) + + // exists with shortname should be true + exists, err = images.Exists(connText, alpine.shortName) + Expect(err).To(BeNil()) + Expect(exists).To(BeTrue()) + + // exists with fqname should be true + exists, err = images.Exists(connText, alpine.name) + Expect(err).To(BeNil()) + Expect(exists).To(BeTrue()) + }) + + It("Load|Import Image", func() { + // load an image + _, err := images.Remove(connText, alpine.name, nil) + Expect(err).To(BeNil()) + exists, err := images.Exists(connText, alpine.name) + Expect(err).To(BeNil()) + Expect(exists).To(BeFalse()) + f, err := os.Open(filepath.Join(ImageCacheDir, alpine.tarballName)) + defer f.Close() + Expect(err).To(BeNil()) + names, err := images.Load(connText, f, nil) + Expect(err).To(BeNil()) + Expect(names).To(Equal(alpine.name)) + exists, err = images.Exists(connText, alpine.name) + Expect(err).To(BeNil()) + Expect(exists).To(BeTrue()) + + // load with a repo name + f, err = os.Open(filepath.Join(ImageCacheDir, alpine.tarballName)) + Expect(err).To(BeNil()) + _, err = images.Remove(connText, alpine.name, nil) + Expect(err).To(BeNil()) + exists, err = images.Exists(connText, alpine.name) + Expect(err).To(BeNil()) + Expect(exists).To(BeFalse()) + newName := "quay.io/newname:fizzle" + names, err = images.Load(connText, f, &newName) + Expect(err).To(BeNil()) + Expect(names).To(Equal(alpine.name)) + exists, err = images.Exists(connText, newName) + Expect(err).To(BeNil()) + Expect(exists).To(BeTrue()) + + // load with a bad repo name should trigger a 500 + f, err = os.Open(filepath.Join(ImageCacheDir, alpine.tarballName)) + Expect(err).To(BeNil()) + _, err = images.Remove(connText, alpine.name, nil) + Expect(err).To(BeNil()) + exists, err = images.Exists(connText, alpine.name) + Expect(err).To(BeNil()) + Expect(exists).To(BeFalse()) + badName := "quay.io/newName:fizzle" + _, err = images.Load(connText, f, &badName) + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) + }) + + It("Export Image", func() { + // Export an image + exportPath := filepath.Join(bt.tempDirPath, alpine.tarballName) + w, err := os.Create(filepath.Join(bt.tempDirPath, alpine.tarballName)) + defer w.Close() + Expect(err).To(BeNil()) + err = images.Export(connText, alpine.name, w, nil, nil) + Expect(err).To(BeNil()) + _, err = os.Stat(exportPath) + Expect(err).To(BeNil()) + + // TODO how do we verify that a format change worked? + }) + + It("Import Image", func() { + // load an image + _, err = images.Remove(connText, alpine.name, nil) + Expect(err).To(BeNil()) + exists, err := images.Exists(connText, alpine.name) + Expect(err).To(BeNil()) + Expect(exists).To(BeFalse()) + f, err := os.Open(filepath.Join(ImageCacheDir, alpine.tarballName)) + defer f.Close() + Expect(err).To(BeNil()) + changes := []string{"CMD /bin/foobar"} + testMessage := "test_import" + _, err = images.Import(connText, changes, &testMessage, &alpine.name, nil, f) + Expect(err).To(BeNil()) + exists, err = images.Exists(connText, alpine.name) + Expect(err).To(BeNil()) + Expect(exists).To(BeTrue()) + data, err := images.GetImage(connText, alpine.name, nil) + Expect(err).To(BeNil()) + Expect(data.Comment).To(Equal(testMessage)) + + }) + It("History Image", func() { + // a bogus name should return a 404 + _, err := images.History(connText, "foobar") + Expect(err).To(Not(BeNil())) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusNotFound)) + + var foundID bool + data, err := images.GetImage(connText, alpine.name, nil) + Expect(err).To(BeNil()) + history, err := images.History(connText, alpine.name) + Expect(err).To(BeNil()) + for _, i := range history { + if i.ID == data.ID { + foundID = true + break + } + } + Expect(foundID).To(BeTrue()) + }) + + It("Search for an image", func() { + imgs, err := images.Search(connText, "alpine", nil, nil) + Expect(err).To(BeNil()) + Expect(len(imgs)).To(BeNumerically(">", 1)) + var foundAlpine bool + for _, i := range imgs { + if i.Name == "docker.io/library/alpine" { + foundAlpine = true + break + } + } + Expect(foundAlpine).To(BeTrue()) + + // Search for alpine with a limit of 10 + ten := 10 + imgs, err = images.Search(connText, "docker.io/alpine", &ten, nil) + Expect(err).To(BeNil()) + Expect(len(imgs)).To(BeNumerically("<=", 10)) + + // Search for alpine with stars greater than 100 + filters := make(map[string][]string) + filters["stars"] = []string{"100"} + imgs, err = images.Search(connText, "docker.io/alpine", nil, filters) + Expect(err).To(BeNil()) + for _, i := range imgs { + Expect(i.Stars).To(BeNumerically(">=", 100)) + } + + // Search with a fqdn + imgs, err = images.Search(connText, "quay.io/libpod/alpine_nginx", nil, nil) + Expect(len(imgs)).To(BeNumerically(">=", 1)) + }) + }) diff --git a/pkg/bindings/test/pods_test.go b/pkg/bindings/test/pods_test.go new file mode 100644 index 000000000..4bea2f8d7 --- /dev/null +++ b/pkg/bindings/test/pods_test.go @@ -0,0 +1,196 @@ +package test_bindings + +import ( + "context" + "net/http" + "time" + + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/bindings/pods" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Podman images", func() { + var ( + bt *bindingTest + s *gexec.Session + connText context.Context + newpod string + err error + trueFlag bool = true + ) + + BeforeEach(func() { + bt = newBindingTest() + newpod = "newpod" + bt.RestoreImagesFromCache() + bt.Podcreate(&newpod) + s = bt.startAPIService() + time.Sleep(1 * time.Second) + connText, err = bindings.NewConnection(context.Background(), bt.sock) + Expect(err).To(BeNil()) + }) + + AfterEach(func() { + s.Kill() + bt.cleanup() + }) + + It("inspect pod", func() { + //Inspect an invalid pod name + _, err := pods.Inspect(connText, "dummyname") + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusNotFound)) + + //Inspect an valid pod name + response, err := pods.Inspect(connText, newpod) + Expect(err).To(BeNil()) + Expect(response.Config.Name).To(Equal(newpod)) + }) + + // Test validates the list all api returns + It("list pod", func() { + //List all the pods in the current instance + podSummary, err := pods.List(connText, nil) + Expect(err).To(BeNil()) + Expect(len(podSummary)).To(Equal(1)) + // Adding an alpine container to the existing pod + bt.RunTopContainer(nil, &trueFlag, &newpod) + podSummary, err = pods.List(connText, nil) + // Verify no errors. + Expect(err).To(BeNil()) + // Verify number of containers in the pod. + Expect(len(podSummary[0].Containers)).To(Equal(2)) + + // Add multiple pods and verify them by name and size. + var newpod2 string = "newpod2" + bt.Podcreate(&newpod2) + podSummary, err = pods.List(connText, nil) + Expect(len(podSummary)).To(Equal(2)) + var names []string + for _, i := range podSummary { + names = append(names, i.Config.Name) + } + Expect(StringInSlice(newpod, names)).To(BeTrue()) + Expect(StringInSlice("newpod2", names)).To(BeTrue()) + + // TODO not working Because: code to list based on filter + // "not yet implemented", + // Validate list pod with filters + //filters := make(map[string][]string) + //filters["name"] = []string{newpod} + //filteredPods, err := pods.List(connText, filters) + //Expect(err).To(BeNil()) + //Expect(len(filteredPods)).To(BeNumerically("==", 1)) + }) + + // The test validates if the exists responds + It("exists pod", func() { + response, err := pods.Exists(connText, "dummyName") + Expect(err).To(BeNil()) + Expect(response).To(BeFalse()) + + // Should exit with no error and response should be true + response, err = pods.Exists(connText, "newpod") + Expect(err).To(BeNil()) + Expect(response).To(BeTrue()) + }) + + // This test validates if All running containers within + // each specified pod are paused and unpaused + It("pause upause pod", func() { + // Pause invalid container + err := pods.Pause(connText, "dummyName") + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusNotFound)) + + // Adding an alpine container to the existing pod + bt.RunTopContainer(nil, &trueFlag, &newpod) + response, err := pods.Inspect(connText, newpod) + Expect(err).To(BeNil()) + + // Binding needs to be modified to inspect the pod state. + // Since we dont have a pod state we inspect the states of the containers within the pod. + // Pause a valid container + err = pods.Pause(connText, newpod) + Expect(err).To(BeNil()) + response, err = pods.Inspect(connText, newpod) + for _, i := range response.Containers { + Expect(define.StringToContainerStatus(i.State)). + To(Equal(define.ContainerStatePaused)) + } + + // Unpause a valid container + err = pods.Unpause(connText, newpod) + Expect(err).To(BeNil()) + response, err = pods.Inspect(connText, newpod) + for _, i := range response.Containers { + Expect(define.StringToContainerStatus(i.State)). + To(Equal(define.ContainerStateRunning)) + } + }) + + It("start stop restart pod", func() { + // Start an invalid pod + err = pods.Start(connText, "dummyName") + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusNotFound)) + + // Stop an invalid pod + err = pods.Stop(connText, "dummyName", nil) + Expect(err).ToNot(BeNil()) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusNotFound)) + + // Restart an invalid pod + err = pods.Restart(connText, "dummyName") + Expect(err).ToNot(BeNil()) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusNotFound)) + + // Start a valid pod and inspect status of each container + err = pods.Start(connText, newpod) + Expect(err).To(BeNil()) + + response, err := pods.Inspect(connText, newpod) + for _, i := range response.Containers { + Expect(define.StringToContainerStatus(i.State)). + To(Equal(define.ContainerStateRunning)) + } + + // Start an already running pod + err = pods.Start(connText, newpod) + Expect(err).To(BeNil()) + + // Stop the running pods + err = pods.Stop(connText, newpod, nil) + Expect(err).To(BeNil()) + response, _ = pods.Inspect(connText, newpod) + for _, i := range response.Containers { + Expect(define.StringToContainerStatus(i.State)). + To(Equal(define.ContainerStateStopped)) + } + + // Stop an already stopped pod + err = pods.Stop(connText, newpod, nil) + Expect(err).To(BeNil()) + + err = pods.Restart(connText, newpod) + Expect(err).To(BeNil()) + response, _ = pods.Inspect(connText, newpod) + for _, i := range response.Containers { + Expect(define.StringToContainerStatus(i.State)). + To(Equal(define.ContainerStateRunning)) + } + }) + + // Remove all stopped pods and their container to be implemented. + It("prune pod", func() { + }) +}) diff --git a/pkg/bindings/test/volumes_test.go b/pkg/bindings/test/volumes_test.go new file mode 100644 index 000000000..c8940d46e --- /dev/null +++ b/pkg/bindings/test/volumes_test.go @@ -0,0 +1,174 @@ +package test_bindings + +import ( + "context" + "fmt" + "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/bindings/containers" + "github.com/containers/libpod/pkg/bindings/volumes" + "net/http" + "time" + + "github.com/containers/libpod/pkg/bindings" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Podman volumes", func() { + var ( + //tempdir string + //err error + //podmanTest *PodmanTestIntegration + bt *bindingTest + s *gexec.Session + connText context.Context + err error + trueFlag = true + ) + + BeforeEach(func() { + //tempdir, err = CreateTempDirInTempDir() + //if err != nil { + // os.Exit(1) + //} + //podmanTest = PodmanTestCreate(tempdir) + //podmanTest.Setup() + //podmanTest.SeedImages() + bt = newBindingTest() + bt.RestoreImagesFromCache() + s = bt.startAPIService() + time.Sleep(1 * time.Second) + connText, err = bindings.NewConnection(context.Background(), bt.sock) + Expect(err).To(BeNil()) + }) + + AfterEach(func() { + //podmanTest.Cleanup() + //f := CurrentGinkgoTestDescription() + //processTestResult(f) + s.Kill() + bt.cleanup() + }) + + It("create volume", func() { + // create a volume with blank config should work + _, err := volumes.Create(connText, handlers.VolumeCreateConfig{}) + Expect(err).To(BeNil()) + + vcc := handlers.VolumeCreateConfig{ + Name: "foobar", + Label: nil, + Opts: nil, + } + vol, err := volumes.Create(connText, vcc) + Expect(err).To(BeNil()) + Expect(vol.Name).To(Equal("foobar")) + + // create volume with same name should 500 + _, err = volumes.Create(connText, vcc) + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) + }) + + It("inspect volume", func() { + vol, err := volumes.Create(connText, handlers.VolumeCreateConfig{}) + Expect(err).To(BeNil()) + data, err := volumes.Inspect(connText, vol.Name) + Expect(err).To(BeNil()) + Expect(data.Name).To(Equal(vol.Name)) + }) + + It("remove volume", func() { + // removing a bogus volume should result in 404 + err := volumes.Remove(connText, "foobar", nil) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusNotFound)) + + // Removing an unused volume should work + vol, err := volumes.Create(connText, handlers.VolumeCreateConfig{}) + Expect(err).To(BeNil()) + err = volumes.Remove(connText, vol.Name, nil) + Expect(err).To(BeNil()) + + // Removing a volume that is being used without force should be 409 + vol, err = volumes.Create(connText, handlers.VolumeCreateConfig{}) + Expect(err).To(BeNil()) + session := bt.runPodman([]string{"run", "-dt", "-v", fmt.Sprintf("%s:/foobar", vol.Name), "--name", "vtest", alpine.name, "top"}) + session.Wait(45) + err = volumes.Remove(connText, vol.Name, nil) + Expect(err).ToNot(BeNil()) + code, _ = bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusConflict)) + + // Removing with a volume in use with force should work with a stopped container + zero := 0 + err = containers.Stop(connText, "vtest", &zero) + Expect(err).To(BeNil()) + err = volumes.Remove(connText, vol.Name, &trueFlag) + Expect(err).To(BeNil()) + }) + + It("list volumes", func() { + // no volumes should be ok + vols, err := volumes.List(connText, nil) + Expect(err).To(BeNil()) + Expect(len(vols)).To(BeZero()) + + // create a bunch of named volumes and make verify with list + volNames := []string{"homer", "bart", "lisa", "maggie", "marge"} + for i := 0; i < 5; i++ { + _, err = volumes.Create(connText, handlers.VolumeCreateConfig{Name: volNames[i]}) + Expect(err).To(BeNil()) + } + vols, err = volumes.List(connText, nil) + Expect(err).To(BeNil()) + Expect(len(vols)).To(BeNumerically("==", 5)) + for _, v := range vols { + Expect(StringInSlice(v.Name, volNames)).To(BeTrue()) + } + + // list with bad filter should be 500 + filters := make(map[string][]string) + filters["foobar"] = []string{"1234"} + _, err = volumes.List(connText, filters) + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) + + filters = make(map[string][]string) + filters["name"] = []string{"homer"} + vols, err = volumes.List(connText, filters) + Expect(err).To(BeNil()) + Expect(len(vols)).To(BeNumerically("==", 1)) + Expect(vols[0].Name).To(Equal("homer")) + }) + + // TODO we need to add filtering to tests + It("prune unused volume", func() { + // Pruning when no volumes present should be ok + _, err := volumes.Prune(connText) + Expect(err).To(BeNil()) + + // Removing an unused volume should work + _, err = volumes.Create(connText, handlers.VolumeCreateConfig{}) + Expect(err).To(BeNil()) + vols, err := volumes.Prune(connText) + Expect(err).To(BeNil()) + Expect(len(vols)).To(BeNumerically("==", 1)) + + _, err = volumes.Create(connText, handlers.VolumeCreateConfig{Name: "homer"}) + Expect(err).To(BeNil()) + _, err = volumes.Create(connText, handlers.VolumeCreateConfig{}) + Expect(err).To(BeNil()) + session := bt.runPodman([]string{"run", "-dt", "-v", fmt.Sprintf("%s:/homer", "homer"), "--name", "vtest", alpine.name, "top"}) + session.Wait(45) + vols, err = volumes.Prune(connText) + Expect(err).To(BeNil()) + Expect(len(vols)).To(BeNumerically("==", 1)) + _, err = volumes.Inspect(connText, "homer") + Expect(err).To(BeNil()) + }) + +}) diff --git a/pkg/bindings/volumes/volumes.go b/pkg/bindings/volumes/volumes.go index 05a4f73fd..0bc818605 100644 --- a/pkg/bindings/volumes/volumes.go +++ b/pkg/bindings/volumes/volumes.go @@ -3,28 +3,35 @@ package volumes import ( "context" "net/http" + "net/url" "strconv" + "strings" "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/bindings" + jsoniter "github.com/json-iterator/go" ) // 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 +func Create(ctx context.Context, config handlers.VolumeCreateConfig) (*libpod.VolumeConfig, error) { var ( - volumeID string + v libpod.VolumeConfig ) - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { - return "", err + return nil, err } - response, err := conn.DoRequest(nil, http.MethodPost, "/volumes/create", nil) + createString, err := jsoniter.MarshalToString(config) if err != nil { - return volumeID, err + return nil, err } - return volumeID, response.Process(&volumeID) + stringReader := strings.NewReader(createString) + response, err := conn.DoRequest(stringReader, http.MethodPost, "/volumes/create", nil) + if err != nil { + return nil, err + } + return &v, response.Process(&v) } // Inspect returns low-level information about a volume. @@ -32,22 +39,40 @@ func Inspect(ctx context.Context, nameOrID string) (*libpod.InspectVolumeData, e var ( inspect libpod.InspectVolumeData ) - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } - response, err := conn.DoRequest(nil, http.MethodPost, "/volumes/%s/json", nil, nameOrID) + response, err := conn.DoRequest(nil, http.MethodGet, "/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 +// List returns the configurations for existing volumes in the form of a slice. Optionally, filters +// can be used to refine the list of volumes. +func List(ctx context.Context, filters map[string][]string) ([]*libpod.VolumeConfig, error) { + var ( + vols []*libpod.VolumeConfig + ) + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + params := url.Values{} + if len(filters) > 0 { + strFilters, err := bindings.FiltersToString(filters) + if err != nil { + return nil, err + } + params.Set("filters", strFilters) + } + response, err := conn.DoRequest(nil, http.MethodGet, "/volumes/json", params) + if err != nil { + return vols, err + } + return vols, response.Process(&vols) } // Prune removes unused volumes from the local filesystem. @@ -55,7 +80,7 @@ func Prune(ctx context.Context) ([]string, error) { var ( pruned []string ) - conn, err := bindings.GetConnectionFromContext(ctx) + conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } @@ -69,15 +94,15 @@ func Prune(ctx context.Context) ([]string, error) { // 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) + conn, err := bindings.GetClient(ctx) if err != nil { return err } - params := make(map[string]string) + params := url.Values{} if force != nil { - params["force"] = strconv.FormatBool(*force) + params.Set("force", strconv.FormatBool(*force)) } - response, err := conn.DoRequest(nil, http.MethodPost, "/volumes/%s/prune", params, nameOrID) + response, err := conn.DoRequest(nil, http.MethodDelete, "/volumes/%s", params, nameOrID) if err != nil { return err } |