Initial commit

This commit is contained in:
Ria Bhatia
2017-12-04 13:32:57 -06:00
committed by Erik St. Martin
commit 0075e5b0f3
9056 changed files with 2523100 additions and 0 deletions

9
vendor/github.com/hyperhq/hyper-api/.travis.yml generated vendored Normal file
View File

@@ -0,0 +1,9 @@
---
language: go
sudo: false
notifications:
email: false
go:
- 1.7
install: make deps
script: make validate && make test

141
vendor/github.com/hyperhq/hyper-api/CHANGELOG.md generated vendored Normal file
View File

@@ -0,0 +1,141 @@
# Changelog
Items starting with DEPRECATE are important deprecation notices. For more information on the list of deprecated APIs please have a look at https://docs.docker.com/misc/deprecated/ where target removal dates can also be found.
## 0.3.2 (2016-03-30)
## Client
- Revert setting the ServerName in the TLS configuration at client init. See https://github.com/docker/swarm/issues/2027.
## 0.3.1 (2016-03-23)
### Client
- Ensure that API paths are properly escaped.
## 0.3.0 (2016-03-22)
### Client
- Add context to every function.
- Fix issue loading a default TLS CA.
- Allow to configure the client with a given http.Client.
- Add support for Windows named pipes.
- Set default host for Solaris.
- Add quiet flag for image load.
- Add ability to hijack connections through a proxy.
- Correctly set content type for image load.
- Add support for getting token for login.
### Types
- Add struct for update restart policy.
- Add human friendly State for container.
- Use OS specific host when DOCKER_HOST is not set.
- Rename Status in info to SystemStatus.
- Add internal flag to network inspect.
- Add disk quota field to container.
- Add EnableIPv6 fields.
- Add Mounts to container.
- Add cgroup driver to info.
- Add userns to host config.
- Remove email from AuthConfig.
- Make AuthConfig fields optional.
- Add IO resource settings for Windows.
- Add storage driver to host config.
- Update NetworkName to return proper user defined network names.
- Support joining cgroups by container id.
- Add KernelMemory to info.
- Add UsernsMode to container config.
- Add CPU resource control for Windows.
- Add AutoRemove to host config.
- Add Status field to Volume.
- Add Label to Image, Network and Volume.
- Add RootFS to container.
## 0.2.3 (2016-02-02)
### Types
- Add missing status field.
## 0.2.2 (2016-01-13)
### Client
- Fix issue configuring response hijacking with TLS enabled.
## 0.2.1 (2016-01-12)
### Client
- Fix issue detecting missing images on container creation.
### Types
- Remove invalid json tag in endpoint configuration.
- Add missing fields in info structure.
## 0.2.0 (2016-01-11)
### Client
- Allow to force network disconnection. (docker 1.10)
### Types
- Add global and local alias configuration to network endpoint.
- Add network ID to network endpoint.
- Add IPAM options.
- Add Seccomp options.
- Fix issue referencing OOMKillDisable.
## 0.1.3 (2016-01-07)
### Client
- Fix issue sending all network configurations for a per network request.
## 0.1.2 (2016-01-07)
### Client
- Add interface to represent the API client.
- Restrict the fields send to the update endpoint to only those that are used.
- Send network settings as part of the container create request. (docker 1.10)
- Send network settings as part of the network connect request. (docker 1.10)
### Types
- Add PidsLimit as part of the host configuration.
- Add PidsStats to show PID stats.
- Add graph storage options to host configuration.
- Add NetworkConfig and EndpointIPAMConfig structs. (docker 1.10)
## 0.1.1 (2016-01-06)
### Client
- Delegate shmSize units conversion to the consumer.
### Types
- Add warnings to the volume list response.
- Fix image build options:
* use 0 as default value for shmSize.
## 0.1.0 (2016-01-04)
### Client
- Initial API client implementation.
### Types
- Initial API types implementation.

55
vendor/github.com/hyperhq/hyper-api/CONTRIBUTING.md generated vendored Normal file
View File

@@ -0,0 +1,55 @@
# Contributing to Docker
### Sign your work
The sign-off is a simple line at the end of the explanation for the patch. Your
signature certifies that you wrote the patch or otherwise have the right to pass
it on as an open-source patch. The rules are pretty simple: if you can certify
the below (from [developercertificate.org](http://developercertificate.org/)):
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
Then you just add a line to every git commit message:
Signed-off-by: Joe Smith <joe.smith@email.com>
Use your real name (sorry, no pseudonyms or anonymous contributions.)
If you set your `user.name` and `user.email` git configs, you can sign your
commit automatically with `git commit -s`.

191
vendor/github.com/hyperhq/hyper-api/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2015-2016 Docker, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

160
vendor/github.com/hyperhq/hyper-api/MAINTAINERS generated vendored Normal file
View File

@@ -0,0 +1,160 @@
# engine-api maintainers file
#
# This file describes who runs the docker/engine-api project and how.
# This is a living document - if you see something out of date or missing, speak up!
#
# It is structured to be consumable by both humans and programs.
# To extract its contents programmatically, use any TOML-compliant parser.
#
# This file is compiled into the MAINTAINERS file in docker/opensource.
#
[Org]
[Org."Core maintainers"]
people = [
"aaronlehmann",
"calavera",
"coolljt0725",
"cpuguy83",
"crosbymichael",
"dnephin",
"dongluochen",
"duglin",
"estesp",
"icecrime",
"jhowardmsft",
"lk4d4",
"mavenugo",
"mhbauer",
"runcom",
"stevvooe",
"thajeztah",
"tianon",
"tibor",
"tonistiigi",
"unclejack",
"vdemeester",
"vieux"
]
[people]
# A reference list of all people associated with the project.
# All other sections should refer to people by their canonical key
# in the people section.
# ADD YOURSELF HERE IN ALPHABETICAL ORDER
[people.aaronlehmann]
Name = "Aaron Lehmann"
Email = "aaron.lehmann@docker.com"
GitHub = "aaronlehmann"
[people.calavera]
Name = "David Calavera"
Email = "david.calavera@gmail.com"
GitHub = "calavera"
[people.coolljt0725]
Name = "Lei Jitang"
Email = "leijitang@huawei.com"
GitHub = "coolljt0725"
[people.cpuguy83]
Name = "Brian Goff"
Email = "cpuguy83@gmail.com"
Github = "cpuguy83"
[people.crosbymichael]
Name = "Michael Crosby"
Email = "crosbymichael@gmail.com"
GitHub = "crosbymichael"
[people.dnephin]
Name = "Daniel Nephin"
Email = "dnephin@gmail.com"
GitHub = "dnephin"
[people.dongluochen]
Name = "Dongluo Chen"
Email = "dongluo.chen@docker.com"
GitHub = "dongluochen"
[people.duglin]
Name = "Doug Davis"
Email = "dug@us.ibm.com"
GitHub = "duglin"
[people.estesp]
Name = "Phil Estes"
Email = "estesp@linux.vnet.ibm.com"
GitHub = "estesp"
[people.icecrime]
Name = "Arnaud Porterie"
Email = "arnaud@docker.com"
GitHub = "icecrime"
[people.jhowardmsft]
Name = "John Howard"
Email = "jhoward@microsoft.com"
GitHub = "jhowardmsft"
[people.lk4d4]
Name = "Alexander Morozov"
Email = "lk4d4@docker.com"
GitHub = "lk4d4"
[people.mavenugo]
Name = "Madhu Venugopal"
Email = "madhu@docker.com"
GitHub = "mavenugo"
[people.mhbauer]
Name = "Morgan Bauer"
Email = "mbauer@us.ibm.com"
GitHub = "mhbauer"
[people.runcom]
Name = "Antonio Murdaca"
Email = "runcom@redhat.com"
GitHub = "runcom"
[people.stevvooe]
Name = "Stephen Day"
Email = "stephen.day@docker.com"
GitHub = "stevvooe"
[people.thajeztah]
Name = "Sebastiaan van Stijn"
Email = "github@gone.nl"
GitHub = "thaJeztah"
[people.tianon]
Name = "Tianon Gravi"
Email = "admwiggin@gmail.com"
GitHub = "tianon"
[people.tibor]
Name = "Tibor Vass"
Email = "tibor@docker.com"
GitHub = "tiborvass"
[people.tonistiigi]
Name = "Tõnis Tiigi"
Email = "tonis@docker.com"
GitHub = "tonistiigi"
[people.unclejack]
Name = "Cristian Staretu"
Email = "cristian.staretu@gmail.com"
GitHub = "unclejack"
[people.vdemeester]
Name = "Vincent Demeester"
Email = "vincent@sbr.pm"
GitHub = "vdemeester"
[people.vieux]
Name = "Victor Vieux"
Email = "vieux@docker.com"
GitHub = "vieux"

21
vendor/github.com/hyperhq/hyper-api/Makefile generated vendored Normal file
View File

@@ -0,0 +1,21 @@
.PHONY: all deps test validate lint
all: deps test validate
deps:
go get -t ./...
go get -u github.com/golang/lint/golint
test:
go test -tags experimental -race -cover ./...
validate: lint
go vet ./...
test -z "$(gofmt -s -l . | tee /dev/stderr)"
lint:
out="$$(golint ./...)"; \
if [ -n "$$(golint ./...)" ]; then \
echo "$$out"; \
exit 1; \
fi

55
vendor/github.com/hyperhq/hyper-api/README.md generated vendored Normal file
View File

@@ -0,0 +1,55 @@
# Go client for the Hyper.sh API
The `hyper` command uses this package to communicate with the Hyper.sh server. It can also be used by your own Go applications to do anything the command-line interface does  running containers, pulling images, managing volumes, etc.
For example, to list running containers (the equivalent of `hyper ps`):
```go
package main
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"github.com/hyperhq/hyper-api/client"
"github.com/hyperhq/hyper-api/types"
)
func main() {
var (
host = "tcp://us-west-1.hyper.sh:443"
customHeaders = map[string]string{}
verStr = "v1.23"
accessKey = "xx"
secretKey = "xxx"
)
httpClient := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
client, err := client.NewClient(host, verStr, httpClient, customHeaders, accessKey, secretKey)
if err != nil {
panic(err)
}
containers, err := client.ContainerList(context.Background(), types.ContainerListOptions{})
if err != nil {
panic(err)
}
for _, container := range containers {
fmt.Printf("%s\t%s\n", container.ID[:10], container.Image)
}
}
```
Full documentation is available on [document](https://docs.hyper.sh).
## License
engine-api is licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for the full license text.

37
vendor/github.com/hyperhq/hyper-api/appveyor.yml generated vendored Normal file
View File

@@ -0,0 +1,37 @@
version: "{build}"
# Source Config
clone_folder: c:\gopath\src\github.com\docker\engine-api
# Build host
environment:
GOPATH: c:\gopath
GOVERSION: 1.6
init:
- git config --global core.autocrlf input
# Build
install:
# Install Go 1.6.
- rmdir c:\go /s /q
- appveyor DownloadFile https://storage.googleapis.com/golang/go%GOVERSION%.windows-amd64.msi
- msiexec /i go%GOVERSION%.windows-amd64.msi /q
- set Path=c:\go\bin;c:\gopath\bin;%Path%
- go version
- go env
build: false
deploy: false
before_test:
- go get -t ./...
- go get github.com/golang/lint/golint
test_script:
- go vet ./...
- golint ./...
- gofmt -s -l .
- go test -race -cover -v -tags=test ./...

View File

@@ -0,0 +1,14 @@
package client
import (
"context"
"github.com/hyperhq/hyper-api/types"
)
// CheckpointCreate creates a checkpoint from the given container with the given name
func (cli *Client) CheckpointCreate(ctx context.Context, container string, options types.CheckpointCreateOptions) error {
resp, err := cli.post(ctx, "/containers/"+container+"/checkpoints", nil, options, nil)
ensureReaderClosed(resp)
return err
}

View File

@@ -0,0 +1,73 @@
package client
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"github.com/hyperhq/hyper-api/types"
)
func TestCheckpointCreateError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
err := client.CheckpointCreate(context.Background(), "nothing", types.CheckpointCreateOptions{
CheckpointID: "noting",
Exit: true,
})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestCheckpointCreate(t *testing.T) {
expectedContainerID := "container_id"
expectedCheckpointID := "checkpoint_id"
expectedURL := "/containers/container_id/checkpoints"
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
if req.Method != "POST" {
return nil, fmt.Errorf("expected POST method, got %s", req.Method)
}
createOptions := &types.CheckpointCreateOptions{}
if err := json.NewDecoder(req.Body).Decode(createOptions); err != nil {
return nil, err
}
if createOptions.CheckpointID != expectedCheckpointID {
return nil, fmt.Errorf("expected CheckpointID to be 'checkpoint_id', got %v", createOptions.CheckpointID)
}
if !createOptions.Exit {
return nil, fmt.Errorf("expected Exit to be true")
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
}, nil
}),
}
err := client.CheckpointCreate(context.Background(), expectedContainerID, types.CheckpointCreateOptions{
CheckpointID: expectedCheckpointID,
Exit: true,
})
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,10 @@
package client
import "context"
// CheckpointDelete deletes the checkpoint with the given name from the given container
func (cli *Client) CheckpointDelete(ctx context.Context, containerID string, checkpointID string) error {
resp, err := cli.delete(ctx, "/containers/"+containerID+"/checkpoints/"+checkpointID, nil, nil)
ensureReaderClosed(resp)
return err
}

View File

@@ -0,0 +1,47 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"context"
)
func TestCheckpointDeleteError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
err := client.CheckpointDelete(context.Background(), "container_id", "checkpoint_id")
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestCheckpointDelete(t *testing.T) {
expectedURL := "/containers/container_id/checkpoints/checkpoint_id"
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
if req.Method != "DELETE" {
return nil, fmt.Errorf("expected DELETE method, got %s", req.Method)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
}, nil
}),
}
err := client.CheckpointDelete(context.Background(), "container_id", "checkpoint_id")
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,22 @@
package client
import (
"encoding/json"
"context"
"github.com/hyperhq/hyper-api/types"
)
// CheckpointList returns the volumes configured in the docker host.
func (cli *Client) CheckpointList(ctx context.Context, container string) ([]types.Checkpoint, error) {
var checkpoints []types.Checkpoint
resp, err := cli.get(ctx, "/containers/"+container+"/checkpoints", nil, nil)
if err != nil {
return checkpoints, err
}
err = json.NewDecoder(resp.body).Decode(&checkpoints)
ensureReaderClosed(resp)
return checkpoints, err
}

View File

@@ -0,0 +1,57 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"context"
"github.com/hyperhq/hyper-api/types"
)
func TestCheckpointListError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.CheckpointList(context.Background(), "container_id")
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestCheckpointList(t *testing.T) {
expectedURL := "/containers/container_id/checkpoints"
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
content, err := json.Marshal([]types.Checkpoint{
{
Name: "checkpoint",
},
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(content)),
}, nil
}),
}
checkpoints, err := client.CheckpointList(context.Background(), "container_id")
if err != nil {
t.Fatal(err)
}
if len(checkpoints) != 1 {
t.Fatalf("expected 1 checkpoint, got %v", checkpoints)
}
}

153
vendor/github.com/hyperhq/hyper-api/client/client.go generated vendored Normal file
View File

@@ -0,0 +1,153 @@
package client
import (
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/docker/go-connections/tlsconfig"
"github.com/hyperhq/hyper-api/client/transport"
)
// Client is the API client that performs all operations
// against a docker server.
type Client struct {
// proto holds the client protocol i.e. unix.
proto string
// addr holds the client address.
addr string
// basePath holds the path to prepend to the requests.
basePath string
// transport is the interface to send request with, it implements transport.Client.
transport transport.Client
// Cloud's Config
accessKey string
secretKey string
// version of the server to talk to.
version string
// custom http headers configured by users.
customHTTPHeaders map[string]string
// region
region string
}
// NewEnvClient initializes a new API client based on environment variables.
// Use DOCKER_HOST to set the url to the docker server.
// Use DOCKER_API_VERSION to set the version of the API to reach, leave empty for latest.
// Use DOCKER_CERT_PATH to load the tls certificates from.
// Use DOCKER_TLS_VERIFY to enable or disable TLS verification, off by default.
func NewEnvClient() (*Client, error) {
var client *http.Client
if dockerCertPath := os.Getenv("DOCKER_CERT_PATH"); dockerCertPath != "" {
options := tlsconfig.Options{
CAFile: filepath.Join(dockerCertPath, "ca.pem"),
CertFile: filepath.Join(dockerCertPath, "cert.pem"),
KeyFile: filepath.Join(dockerCertPath, "key.pem"),
InsecureSkipVerify: os.Getenv("DOCKER_TLS_VERIFY") == "",
}
tlsc, err := tlsconfig.Client(options)
if err != nil {
return nil, err
}
client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsc,
},
}
}
host := os.Getenv("DOCKER_HOST")
if host == "" {
host = DefaultDockerHost
}
accessKey := os.Getenv("ACCESSKEY")
secretKey := os.Getenv("SECRETKEY")
region := os.Getenv("HYPER_REGION")
return NewClient(host, os.Getenv("DOCKER_API_VERSION"), client, nil, accessKey, secretKey, region)
}
// NewClient initializes a new API client for the given host and API version.
// It won't send any version information if the version number is empty.
// It uses the given http client as transport.
// It also initializes the custom http headers to add to each request.
func NewClient(host string, version string, client *http.Client, httpHeaders map[string]string, ak, sk, region string) (*Client, error) {
proto, addr, basePath, err := ParseHost(host)
if err != nil {
return nil, err
}
transport, err := transport.NewTransportWithHTTP(proto, addr, client)
if err != nil {
return nil, err
}
return &Client{
proto: proto,
addr: addr,
basePath: basePath,
transport: transport,
accessKey: ak,
secretKey: sk,
version: version,
customHTTPHeaders: httpHeaders,
region: region,
}, nil
}
// getAPIPath returns the versioned request path to call the api.
// It appends the query parameters to the path if they are not empty.
func (cli *Client) getAPIPath(p string, query url.Values) string {
var apiPath string
if cli.version != "" {
v := strings.TrimPrefix(cli.version, "v")
apiPath = fmt.Sprintf("%s/v%s%s", cli.basePath, v, p)
} else {
apiPath = fmt.Sprintf("%s%s", cli.basePath, p)
}
u := &url.URL{
Path: apiPath,
}
if len(query) > 0 {
u.RawQuery = query.Encode()
}
return u.String()
}
// ClientVersion returns the version string associated with this
// instance of the Client. Note that this value can be changed
// via the DOCKER_API_VERSION env var.
func (cli *Client) ClientVersion() string {
return cli.version
}
// UpdateClientVersion updates the version string associated with this
// instance of the Client.
func (cli *Client) UpdateClientVersion(v string) {
cli.version = v
}
// ParseHost verifies that the given host strings is valid.
func ParseHost(host string) (string, string, string, error) {
protoAddrParts := strings.SplitN(host, "://", 2)
if len(protoAddrParts) == 1 {
return "", "", "", fmt.Errorf("unable to parse docker host `%s`", host)
}
var basePath string
proto, addr := protoAddrParts[0], protoAddrParts[1]
if proto == "tcp" {
parsed, err := url.Parse("tcp://" + addr)
if err != nil {
return "", "", "", err
}
addr = parsed.Host
basePath = parsed.Path
}
return proto, addr, basePath, nil
}

View File

@@ -0,0 +1,4 @@
package client
// DefaultDockerHost defines os specific default if DOCKER_HOST is unset
const DefaultDockerHost = "tcp://127.0.0.1:2375"

View File

@@ -0,0 +1,76 @@
package client
import (
"bytes"
"crypto/tls"
"encoding/json"
"io/ioutil"
"net/http"
"github.com/hyperhq/hyper-api/client/transport"
"github.com/hyperhq/hyper-api/types"
)
type mockClient struct {
do func(*http.Request) (*http.Response, error)
}
// TLSConfig returns the TLS configuration.
func (m *mockClient) TLSConfig() *tls.Config {
return &tls.Config{}
}
// Scheme returns protocol scheme to use.
func (m *mockClient) Scheme() string {
return "http"
}
// Secure returns true if there is a TLS configuration.
func (m *mockClient) Secure() bool {
return false
}
// NewMockClient returns a mocked client that runs the function supplied as `client.Do` call
func newMockClient(tlsConfig *tls.Config, doer func(*http.Request) (*http.Response, error)) transport.Client {
if tlsConfig != nil {
panic("this actually gets set!")
}
return &mockClient{
do: doer,
}
}
// Do executes the supplied function for the mock.
func (m mockClient) Do(req *http.Request) (*http.Response, error) {
return m.do(req)
}
func errorMock(statusCode int, message string) func(req *http.Request) (*http.Response, error) {
return func(req *http.Request) (*http.Response, error) {
header := http.Header{}
header.Set("Content-Type", "application/json")
body, err := json.Marshal(&types.ErrorResponse{
Message: message,
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: statusCode,
Body: ioutil.NopCloser(bytes.NewReader(body)),
Header: header,
}, nil
}
}
func plainTextErrorMock(statusCode int, message string) func(req *http.Request) (*http.Response, error) {
return func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: statusCode,
Body: ioutil.NopCloser(bytes.NewReader([]byte(message))),
}, nil
}
}

View File

@@ -0,0 +1,245 @@
package client
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
"testing"
"context"
"github.com/hyperhq/hyper-api/types"
)
func TestNewEnvClient(t *testing.T) {
cases := []struct {
envs map[string]string
expectedError string
expectedVersion string
}{
{
envs: map[string]string{},
expectedVersion: DefaultVersion,
},
{
envs: map[string]string{
"DOCKER_CERT_PATH": "invalid/path",
},
expectedError: "Could not load X509 key pair: open invalid/path/cert.pem: no such file or directory. Make sure the key is not encrypted",
},
{
envs: map[string]string{
"DOCKER_CERT_PATH": "testdata/",
},
expectedVersion: DefaultVersion,
},
{
envs: map[string]string{
"DOCKER_HOST": "host",
},
expectedError: "unable to parse docker host `host`",
},
{
envs: map[string]string{
"DOCKER_HOST": "invalid://url",
},
expectedVersion: DefaultVersion,
},
{
envs: map[string]string{
"DOCKER_API_VERSION": "anything",
},
expectedVersion: "anything",
},
{
envs: map[string]string{
"DOCKER_API_VERSION": "1.22",
},
expectedVersion: "1.22",
},
}
for _, c := range cases {
recoverEnvs := setupEnvs(t, c.envs)
apiclient, err := NewEnvClient()
if c.expectedError != "" {
if err == nil || err.Error() != c.expectedError {
t.Errorf("expected an error %s, got %s, for %v", c.expectedError, err.Error(), c)
}
} else {
if err != nil {
t.Error(err)
}
version := apiclient.ClientVersion()
if version != c.expectedVersion {
t.Errorf("expected %s, got %s, for %v", c.expectedVersion, version, c)
}
}
recoverEnvs(t)
}
}
func setupEnvs(t *testing.T, envs map[string]string) func(*testing.T) {
oldEnvs := map[string]string{}
for key, value := range envs {
oldEnv := os.Getenv(key)
oldEnvs[key] = oldEnv
err := os.Setenv(key, value)
if err != nil {
t.Error(err)
}
}
return func(t *testing.T) {
for key, value := range oldEnvs {
err := os.Setenv(key, value)
if err != nil {
t.Error(err)
}
}
}
}
func TestGetAPIPath(t *testing.T) {
cases := []struct {
v string
p string
q url.Values
e string
}{
{"", "/containers/json", nil, "/containers/json"},
{"", "/containers/json", url.Values{}, "/containers/json"},
{"", "/containers/json", url.Values{"s": []string{"c"}}, "/containers/json?s=c"},
{"1.22", "/containers/json", nil, "/v1.22/containers/json"},
{"1.22", "/containers/json", url.Values{}, "/v1.22/containers/json"},
{"1.22", "/containers/json", url.Values{"s": []string{"c"}}, "/v1.22/containers/json?s=c"},
{"v1.22", "/containers/json", nil, "/v1.22/containers/json"},
{"v1.22", "/containers/json", url.Values{}, "/v1.22/containers/json"},
{"v1.22", "/containers/json", url.Values{"s": []string{"c"}}, "/v1.22/containers/json?s=c"},
{"v1.22", "/networks/kiwl$%^", nil, "/v1.22/networks/kiwl$%25%5E"},
}
for _, cs := range cases {
c, err := NewClient("unix:///var/run/docker.sock", cs.v, nil, nil)
if err != nil {
t.Fatal(err)
}
g := c.getAPIPath(cs.p, cs.q)
if g != cs.e {
t.Fatalf("Expected %s, got %s", cs.e, g)
}
}
}
func TestParseHost(t *testing.T) {
cases := []struct {
host string
proto string
addr string
base string
err bool
}{
{"", "", "", "", true},
{"foobar", "", "", "", true},
{"foo://bar", "foo", "bar", "", false},
{"tcp://localhost:2476", "tcp", "localhost:2476", "", false},
{"tcp://localhost:2476/path", "tcp", "localhost:2476", "/path", false},
}
for _, cs := range cases {
p, a, b, e := ParseHost(cs.host)
if cs.err && e == nil {
t.Fatalf("expected error, got nil")
}
if !cs.err && e != nil {
t.Fatal(e)
}
if cs.proto != p {
t.Fatalf("expected proto %s, got %s", cs.proto, p)
}
if cs.addr != a {
t.Fatalf("expected addr %s, got %s", cs.addr, a)
}
if cs.base != b {
t.Fatalf("expected base %s, got %s", cs.base, b)
}
}
}
func TestUpdateClientVersion(t *testing.T) {
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
splitQuery := strings.Split(req.URL.Path, "/")
queryVersion := splitQuery[1]
b, err := json.Marshal(types.Version{
APIVersion: queryVersion,
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(b)),
}, nil
}),
}
cases := []struct {
v string
}{
{"1.20"},
{"v1.21"},
{"1.22"},
{"v1.22"},
}
for _, cs := range cases {
client.UpdateClientVersion(cs.v)
r, err := client.ServerVersion(context.Background())
if err != nil {
t.Fatal(err)
}
if strings.TrimPrefix(r.APIVersion, "v") != strings.TrimPrefix(cs.v, "v") {
t.Fatalf("Expected %s, got %s", cs.v, r.APIVersion)
}
}
}
func TestNewEnvClientSetsDefaultVersion(t *testing.T) {
// Unset environment variables
envVarKeys := []string{
"DOCKER_HOST",
"DOCKER_API_VERSION",
"DOCKER_TLS_VERIFY",
"DOCKER_CERT_PATH",
}
envVarValues := make(map[string]string)
for _, key := range envVarKeys {
envVarValues[key] = os.Getenv(key)
os.Setenv(key, "")
}
client, err := NewEnvClient()
if err != nil {
t.Fatal(err)
}
if client.version != DefaultVersion {
t.Fatalf("Expected %s, got %s", DefaultVersion, client.version)
}
expected := "1.22"
os.Setenv("DOCKER_API_VERSION", expected)
client, err = NewEnvClient()
if err != nil {
t.Fatal(err)
}
if client.version != expected {
t.Fatalf("Expected %s, got %s", expected, client.version)
}
// Restore environment variables
for _, key := range envVarKeys {
os.Setenv(key, envVarValues[key])
}
}

View File

@@ -0,0 +1,6 @@
// +build linux freebsd solaris openbsd
package client
// DefaultDockerHost defines os specific default if DOCKER_HOST is unset
const DefaultDockerHost = "unix:///var/run/docker.sock"

View File

@@ -0,0 +1,4 @@
package client
// DefaultDockerHost defines os specific default if DOCKER_HOST is unset
const DefaultDockerHost = "npipe:////./pipe/docker_engine"

149
vendor/github.com/hyperhq/hyper-api/client/compose.go generated vendored Normal file
View File

@@ -0,0 +1,149 @@
package client
import (
"context"
"fmt"
"io"
"net/url"
"strings"
"github.com/hyperhq/hyper-api/types"
"github.com/hyperhq/libcompose/config"
)
type composeConfigWrapper struct {
ServiceConfigs *config.ServiceConfigs `json:"ServiceConfigs"`
VolumeConfigs map[string]*config.VolumeConfig `json:"VolumeConfigs"`
NetworkConfigs map[string]*config.NetworkConfig `json:"NetworkConfigs"`
AuthConfigs map[string]types.AuthConfig `json:"auths"`
}
func (cli *Client) ComposeUp(project string, services []string, c *config.ServiceConfigs, vc map[string]*config.VolumeConfig, nc map[string]*config.NetworkConfig, auth map[string]types.AuthConfig, forcerecreate, norecreate bool) (io.ReadCloser, error) {
query := url.Values{}
query.Set("project", project)
if forcerecreate {
query.Set("forcerecreate", "true")
}
if norecreate {
query.Set("norecreate", "true")
}
if len(services) > 0 {
query.Set("services", strings.Join(services, "}{"))
}
body := composeConfigWrapper{
ServiceConfigs: c,
VolumeConfigs: vc,
NetworkConfigs: nc,
AuthConfigs: auth,
}
resp, err := cli.post(context.Background(), "/compose/up", query, body, nil)
if err != nil {
return nil, err
}
return resp.body, nil
}
func (cli *Client) ComposeDown(project string, services []string, rmi string, vol, rmorphans bool) (io.ReadCloser, error) {
query := url.Values{}
query.Set("project", project)
if rmi != "" {
query.Set("rmi", rmi)
}
if vol {
query.Set("rmvol", "true")
}
if rmorphans {
query.Set("rmorphans", "true")
}
if len(services) > 0 {
query.Set("services", strings.Join(services, "}{"))
}
resp, err := cli.post(context.Background(), "/compose/down", query, nil, nil)
if err != nil {
return nil, err
}
return resp.body, nil
}
func (cli *Client) ComposeCreate(project string, services []string, c *config.ServiceConfigs, vc map[string]*config.VolumeConfig, nc map[string]*config.NetworkConfig, auth map[string]types.AuthConfig, forcerecreate, norecreate bool) (io.ReadCloser, error) {
query := url.Values{}
query.Set("project", project)
if forcerecreate {
query.Set("forcerecreate", "true")
}
if norecreate {
query.Set("norecreate", "true")
}
if len(services) > 0 {
query.Set("services", strings.Join(services, "}{"))
}
body := composeConfigWrapper{
ServiceConfigs: c,
VolumeConfigs: vc,
NetworkConfigs: nc,
AuthConfigs: auth,
}
resp, err := cli.post(context.Background(), "/compose/create", query, body, nil)
if err != nil {
return nil, err
}
return resp.body, nil
}
func (cli *Client) ComposeRm(project string, services []string, rmVol bool) (io.ReadCloser, error) {
query := url.Values{}
query.Set("project", project)
if rmVol {
query.Set("rmvol", "true")
}
if len(services) > 0 {
query.Set("services", strings.Join(services, "}{"))
}
resp, err := cli.post(context.Background(), "/compose/rm", query, nil, nil)
if err != nil {
return nil, err
}
return resp.body, nil
}
func (cli *Client) ComposeStart(project string, services []string) (io.ReadCloser, error) {
query := url.Values{}
query.Set("project", project)
if len(services) > 0 {
query.Set("services", strings.Join(services, "}{"))
}
resp, err := cli.post(context.Background(), "/compose/start", query, nil, nil)
if err != nil {
return nil, err
}
return resp.body, nil
}
func (cli *Client) ComposeStop(project string, services []string, timeout int) (io.ReadCloser, error) {
query := url.Values{}
query.Set("project", project)
query.Set("seconds", fmt.Sprintf("%d", timeout))
if len(services) > 0 {
query.Set("services", strings.Join(services, "}{"))
}
resp, err := cli.post(context.Background(), "/compose/stop", query, nil, nil)
if err != nil {
return nil, err
}
return resp.body, nil
}
func (cli *Client) ComposeKill(project string, services []string, signal string) (io.ReadCloser, error) {
query := url.Values{}
query.Set("project", project)
query.Set("signal", signal)
if len(services) > 0 {
query.Set("services", strings.Join(services, "}{"))
}
resp, err := cli.post(context.Background(), "/compose/kill", query, nil, nil)
if err != nil {
return nil, err
}
return resp.body, nil
}

View File

@@ -0,0 +1,34 @@
package client
import (
"context"
"net/url"
"github.com/hyperhq/hyper-api/types"
)
// ContainerAttach attaches a connection to a container in the server.
// It returns a types.HijackedConnection with the hijacked connection
// and the a reader to get output. It's up to the called to close
// the hijacked connection by calling types.HijackedResponse.Close.
func (cli *Client) ContainerAttach(ctx context.Context, container string, options types.ContainerAttachOptions) (types.HijackedResponse, error) {
query := url.Values{}
if options.Stream {
query.Set("stream", "1")
}
if options.Stdin {
query.Set("stdin", "1")
}
if options.Stdout {
query.Set("stdout", "1")
}
if options.Stderr {
query.Set("stderr", "1")
}
if options.DetachKeys != "" {
query.Set("detachKeys", options.DetachKeys)
}
headers := map[string][]string{"Content-Type": {"text/plain"}}
return cli.postHijacked(ctx, "/containers/"+container+"/attach", query, nil, headers)
}

View File

@@ -0,0 +1,53 @@
package client
import (
"context"
"encoding/json"
"errors"
"net/url"
distreference "github.com/docker/distribution/reference"
"github.com/hyperhq/hyper-api/types"
"github.com/hyperhq/hyper-api/types/reference"
)
// ContainerCommit applies changes into a container and creates a new tagged image.
func (cli *Client) ContainerCommit(ctx context.Context, container string, options types.ContainerCommitOptions) (types.ContainerCommitResponse, error) {
var repository, tag string
if options.Reference != "" {
distributionRef, err := distreference.ParseNamed(options.Reference)
if err != nil {
return types.ContainerCommitResponse{}, err
}
if _, isCanonical := distributionRef.(distreference.Canonical); isCanonical {
return types.ContainerCommitResponse{}, errors.New("refusing to create a tag with a digest reference")
}
tag = reference.GetTagFromNamedRef(distributionRef)
repository = distributionRef.Name()
}
query := url.Values{}
query.Set("container", container)
query.Set("repo", repository)
query.Set("tag", tag)
query.Set("comment", options.Comment)
query.Set("author", options.Author)
for _, change := range options.Changes {
query.Add("changes", change)
}
if options.Pause != true {
query.Set("pause", "0")
}
var response types.ContainerCommitResponse
resp, err := cli.post(ctx, "/commit", query, options.Config, nil)
if err != nil {
return response, err
}
err = json.NewDecoder(resp.body).Decode(&response)
ensureReaderClosed(resp)
return response, err
}

View File

@@ -0,0 +1,96 @@
package client
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"github.com/hyperhq/hyper-api/types"
)
func TestContainerCommitError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ContainerCommit(context.Background(), "nothing", types.ContainerCommitOptions{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestContainerCommit(t *testing.T) {
expectedURL := "/commit"
expectedContainerID := "container_id"
specifiedReference := "repository_name:tag"
expectedRepositoryName := "repository_name"
expectedTag := "tag"
expectedComment := "comment"
expectedAuthor := "author"
expectedChanges := []string{"change1", "change2"}
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
query := req.URL.Query()
containerID := query.Get("container")
if containerID != expectedContainerID {
return nil, fmt.Errorf("container id not set in URL query properly. Expected '%s', got %s", expectedContainerID, containerID)
}
repo := query.Get("repo")
if repo != expectedRepositoryName {
return nil, fmt.Errorf("container repo not set in URL query properly. Expected '%s', got %s", expectedRepositoryName, repo)
}
tag := query.Get("tag")
if tag != expectedTag {
return nil, fmt.Errorf("container tag not set in URL query properly. Expected '%s', got %s'", expectedTag, tag)
}
comment := query.Get("comment")
if comment != expectedComment {
return nil, fmt.Errorf("container comment not set in URL query properly. Expected '%s', got %s'", expectedComment, comment)
}
author := query.Get("author")
if author != expectedAuthor {
return nil, fmt.Errorf("container author not set in URL query properly. Expected '%s', got %s'", expectedAuthor, author)
}
pause := query.Get("pause")
if pause != "0" {
return nil, fmt.Errorf("container pause not set in URL query properly. Expected 'true', got %v'", pause)
}
changes := query["changes"]
if len(changes) != len(expectedChanges) {
return nil, fmt.Errorf("expected container changes size to be '%d', got %d", len(expectedChanges), len(changes))
}
b, err := json.Marshal(types.ContainerCommitResponse{
ID: "new_container_id",
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(b)),
}, nil
}),
}
r, err := client.ContainerCommit(context.Background(), expectedContainerID, types.ContainerCommitOptions{
Reference: specifiedReference,
Comment: expectedComment,
Author: expectedAuthor,
Changes: expectedChanges,
Pause: false,
})
if err != nil {
t.Fatal(err)
}
if r.ID != "new_container_id" {
t.Fatalf("expected `container_id`, got %s", r.ID)
}
}

View File

@@ -0,0 +1,96 @@
package client
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"path/filepath"
"strings"
"github.com/hyperhq/hyper-api/types"
)
// ContainerStatPath returns Stat information about a path inside the container filesystem.
func (cli *Client) ContainerStatPath(ctx context.Context, containerID, path string) (types.ContainerPathStat, error) {
query := url.Values{}
query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API.
urlStr := fmt.Sprintf("/containers/%s/archive", containerID)
response, err := cli.head(ctx, urlStr, query, nil)
if err != nil {
return types.ContainerPathStat{}, err
}
defer ensureReaderClosed(response)
return getContainerPathStatFromHeader(response.header)
}
// CopyToContainer copies content into the container filesystem.
func (cli *Client) CopyToContainer(ctx context.Context, container, path string, content io.Reader, options types.CopyToContainerOptions) error {
query := url.Values{}
query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API.
// Do not allow for an existing directory to be overwritten by a non-directory and vice versa.
if !options.AllowOverwriteDirWithFile {
query.Set("noOverwriteDirNonDir", "true")
}
apiPath := fmt.Sprintf("/containers/%s/archive", container)
response, err := cli.putRaw(ctx, apiPath, query, content, nil)
if err != nil {
return err
}
defer ensureReaderClosed(response)
if response.statusCode != http.StatusOK {
return fmt.Errorf("unexpected status code from daemon: %d", response.statusCode)
}
return nil
}
// CopyFromContainer gets the content from the container and returns it as a Reader
// to manipulate it in the host. It's up to the caller to close the reader.
func (cli *Client) CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
query := make(url.Values, 1)
query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API.
apiPath := fmt.Sprintf("/containers/%s/archive", container)
response, err := cli.get(ctx, apiPath, query, nil)
if err != nil {
return nil, types.ContainerPathStat{}, err
}
if response.statusCode != http.StatusOK {
return nil, types.ContainerPathStat{}, fmt.Errorf("unexpected status code from daemon: %d", response.statusCode)
}
// In order to get the copy behavior right, we need to know information
// about both the source and the destination. The response headers include
// stat info about the source that we can use in deciding exactly how to
// copy it locally. Along with the stat info about the local destination,
// we have everything we need to handle the multiple possibilities there
// can be when copying a file/dir from one location to another file/dir.
stat, err := getContainerPathStatFromHeader(response.header)
if err != nil {
return nil, stat, fmt.Errorf("unable to get resource stat from response: %s", err)
}
return response.body, stat, err
}
func getContainerPathStatFromHeader(header http.Header) (types.ContainerPathStat, error) {
var stat types.ContainerPathStat
encodedStat := header.Get("X-Docker-Container-Path-Stat")
statDecoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(encodedStat))
err := json.NewDecoder(statDecoder).Decode(&stat)
if err != nil {
err = fmt.Errorf("unable to decode container path stat header: %s", err)
}
return stat, err
}

View File

@@ -0,0 +1,244 @@
package client
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"context"
"github.com/hyperhq/hyper-api/types"
)
func TestContainerStatPathError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ContainerStatPath(context.Background(), "container_id", "path")
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server error, got %v", err)
}
}
func TestContainerStatPathNoHeaderError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
}, nil
}),
}
_, err := client.ContainerStatPath(context.Background(), "container_id", "path/to/file")
if err == nil {
t.Fatalf("expected an error, got nothing")
}
}
func TestContainerStatPath(t *testing.T) {
expectedURL := "/containers/container_id/archive"
expectedPath := "path/to/file"
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
if req.Method != "HEAD" {
return nil, fmt.Errorf("expected HEAD method, got %s", req.Method)
}
query := req.URL.Query()
path := query.Get("path")
if path != expectedPath {
return nil, fmt.Errorf("path not set in URL query properly")
}
content, err := json.Marshal(types.ContainerPathStat{
Name: "name",
Mode: 0700,
})
if err != nil {
return nil, err
}
base64PathStat := base64.StdEncoding.EncodeToString(content)
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
Header: http.Header{
"X-Docker-Container-Path-Stat": []string{base64PathStat},
},
}, nil
}),
}
stat, err := client.ContainerStatPath(context.Background(), "container_id", expectedPath)
if err != nil {
t.Fatal(err)
}
if stat.Name != "name" {
t.Fatalf("expected container path stat name to be 'name', was '%s'", stat.Name)
}
if stat.Mode != 0700 {
t.Fatalf("expected container path stat mode to be 0700, was '%v'", stat.Mode)
}
}
func TestCopyToContainerError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
err := client.CopyToContainer(context.Background(), "container_id", "path/to/file", bytes.NewReader([]byte("")), types.CopyToContainerOptions{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server error, got %v", err)
}
}
func TestCopyToContainerNotStatusOKError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusNoContent, "No content")),
}
err := client.CopyToContainer(context.Background(), "container_id", "path/to/file", bytes.NewReader([]byte("")), types.CopyToContainerOptions{})
if err == nil || err.Error() != "unexpected status code from daemon: 204" {
t.Fatalf("expected an unexpected status code error, got %v", err)
}
}
func TestCopyToContainer(t *testing.T) {
expectedURL := "/containers/container_id/archive"
expectedPath := "path/to/file"
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
if req.Method != "PUT" {
return nil, fmt.Errorf("expected PUT method, got %s", req.Method)
}
query := req.URL.Query()
path := query.Get("path")
if path != expectedPath {
return nil, fmt.Errorf("path not set in URL query properly, expected '%s', got %s", expectedPath, path)
}
noOverwriteDirNonDir := query.Get("noOverwriteDirNonDir")
if noOverwriteDirNonDir != "true" {
return nil, fmt.Errorf("noOverwriteDirNonDir not set in URL query properly, expected true, got %s", noOverwriteDirNonDir)
}
content, err := ioutil.ReadAll(req.Body)
if err != nil {
return nil, err
}
if err := req.Body.Close(); err != nil {
return nil, err
}
if string(content) != "content" {
return nil, fmt.Errorf("expected content to be 'content', got %s", string(content))
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
}, nil
}),
}
err := client.CopyToContainer(context.Background(), "container_id", expectedPath, bytes.NewReader([]byte("content")), types.CopyToContainerOptions{
AllowOverwriteDirWithFile: false,
})
if err != nil {
t.Fatal(err)
}
}
func TestCopyFromContainerError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, _, err := client.CopyFromContainer(context.Background(), "container_id", "path/to/file")
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server error, got %v", err)
}
}
func TestCopyFromContainerNotStatusOKError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusNoContent, "No content")),
}
_, _, err := client.CopyFromContainer(context.Background(), "container_id", "path/to/file")
if err == nil || err.Error() != "unexpected status code from daemon: 204" {
t.Fatalf("expected an unexpected status code error, got %v", err)
}
}
func TestCopyFromContainerNoHeaderError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
}, nil
}),
}
_, _, err := client.CopyFromContainer(context.Background(), "container_id", "path/to/file")
if err == nil {
t.Fatalf("expected an error, got nothing")
}
}
func TestCopyFromContainer(t *testing.T) {
expectedURL := "/containers/container_id/archive"
expectedPath := "path/to/file"
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
if req.Method != "GET" {
return nil, fmt.Errorf("expected PUT method, got %s", req.Method)
}
query := req.URL.Query()
path := query.Get("path")
if path != expectedPath {
return nil, fmt.Errorf("path not set in URL query properly, expected '%s', got %s", expectedPath, path)
}
headercontent, err := json.Marshal(types.ContainerPathStat{
Name: "name",
Mode: 0700,
})
if err != nil {
return nil, err
}
base64PathStat := base64.StdEncoding.EncodeToString(headercontent)
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte("content"))),
Header: http.Header{
"X-Docker-Container-Path-Stat": []string{base64PathStat},
},
}, nil
}),
}
r, stat, err := client.CopyFromContainer(context.Background(), "container_id", expectedPath)
if err != nil {
t.Fatal(err)
}
if stat.Name != "name" {
t.Fatalf("expected container path stat name to be 'name', was '%s'", stat.Name)
}
if stat.Mode != 0700 {
t.Fatalf("expected container path stat mode to be 0700, was '%v'", stat.Mode)
}
content, err := ioutil.ReadAll(r)
if err != nil {
t.Fatal(err)
}
if err := r.Close(); err != nil {
t.Fatal(err)
}
if string(content) != "content" {
t.Fatalf("expected content to be 'content', got %s", string(content))
}
}

View File

@@ -0,0 +1,46 @@
package client
import (
"encoding/json"
"net/url"
"strings"
"context"
"github.com/hyperhq/hyper-api/types"
"github.com/hyperhq/hyper-api/types/container"
"github.com/hyperhq/hyper-api/types/network"
)
type configWrapper struct {
*container.Config
HostConfig *container.HostConfig
NetworkingConfig *network.NetworkingConfig
}
// ContainerCreate creates a new container based in the given configuration.
// It can be associated with a name, but it's not mandatory.
func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (types.ContainerCreateResponse, error) {
var response types.ContainerCreateResponse
query := url.Values{}
if containerName != "" {
query.Set("name", containerName)
}
body := configWrapper{
Config: config,
HostConfig: hostConfig,
NetworkingConfig: networkingConfig,
}
serverResp, err := cli.post(ctx, "/containers/create", query, body, nil)
if err != nil {
if serverResp != nil && serverResp.statusCode == 404 && strings.Contains(err.Error(), "No such image") {
return response, imageNotFoundError{config.Image}
}
return response, err
}
err = json.NewDecoder(serverResp.body).Decode(&response)
ensureReaderClosed(serverResp)
return response, err
}

View File

@@ -0,0 +1,77 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"context"
"github.com/hyperhq/hyper-api/types"
"github.com/hyperhq/hyper-api/types/container"
)
func TestContainerCreateError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ContainerCreate(context.Background(), nil, nil, nil, "nothing")
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
// 404 doesn't automagitally means an unknown image
client = &Client{
transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")),
}
_, err = client.ContainerCreate(context.Background(), nil, nil, nil, "nothing")
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestContainerCreateImageNotFound(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusNotFound, "No such image")),
}
_, err := client.ContainerCreate(context.Background(), &container.Config{Image: "unknown_image"}, nil, nil, "unknown")
if err == nil || !IsErrImageNotFound(err) {
t.Fatalf("expected an imageNotFound error, got %v", err)
}
}
func TestContainerCreateWithName(t *testing.T) {
expectedURL := "/containers/create"
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
name := req.URL.Query().Get("name")
if name != "container_name" {
return nil, fmt.Errorf("container name not set in URL query properly. Expected `container_name`, got %s", name)
}
b, err := json.Marshal(types.ContainerCreateResponse{
ID: "container_id",
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(b)),
}, nil
}),
}
r, err := client.ContainerCreate(context.Background(), nil, nil, nil, "container_name")
if err != nil {
t.Fatal(err)
}
if r.ID != "container_id" {
t.Fatalf("expected `container_id`, got %s", r.ID)
}
}

View File

@@ -0,0 +1,23 @@
package client
import (
"encoding/json"
"net/url"
"context"
"github.com/hyperhq/hyper-api/types"
)
// ContainerDiff shows differences in a container filesystem since it was started.
func (cli *Client) ContainerDiff(ctx context.Context, containerID string) ([]types.ContainerChange, error) {
var changes []types.ContainerChange
serverResp, err := cli.get(ctx, "/containers/"+containerID+"/changes", url.Values{}, nil)
if err != nil {
return changes, err
}
err = json.NewDecoder(serverResp.body).Decode(&changes)
ensureReaderClosed(serverResp)
return changes, err
}

View File

@@ -0,0 +1,61 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"context"
"github.com/hyperhq/hyper-api/types"
)
func TestContainerDiffError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ContainerDiff(context.Background(), "nothing")
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestContainerDiff(t *testing.T) {
expectedURL := "/containers/container_id/changes"
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
b, err := json.Marshal([]types.ContainerChange{
{
Kind: 0,
Path: "/path/1",
},
{
Kind: 1,
Path: "/path/2",
},
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(b)),
}, nil
}),
}
changes, err := client.ContainerDiff(context.Background(), "container_id")
if err != nil {
t.Fatal(err)
}
if len(changes) != 2 {
t.Fatalf("expected an array of 2 changes, got %v", changes)
}
}

View File

@@ -0,0 +1,49 @@
package client
import (
"encoding/json"
"context"
"github.com/hyperhq/hyper-api/types"
)
// ContainerExecCreate creates a new exec configuration to run an exec process.
func (cli *Client) ContainerExecCreate(ctx context.Context, container string, config types.ExecConfig) (types.ContainerExecCreateResponse, error) {
var response types.ContainerExecCreateResponse
resp, err := cli.post(ctx, "/containers/"+container+"/exec", nil, config, nil)
if err != nil {
return response, err
}
err = json.NewDecoder(resp.body).Decode(&response)
ensureReaderClosed(resp)
return response, err
}
// ContainerExecStart starts an exec process already created in the docker host.
func (cli *Client) ContainerExecStart(ctx context.Context, execID string, config types.ExecStartCheck) error {
resp, err := cli.post(ctx, "/exec/"+execID+"/start", nil, config, nil)
ensureReaderClosed(resp)
return err
}
// ContainerExecAttach attaches a connection to an exec process in the server.
// It returns a types.HijackedConnection with the hijacked connection
// and the a reader to get output. It's up to the called to close
// the hijacked connection by calling types.HijackedResponse.Close.
func (cli *Client) ContainerExecAttach(ctx context.Context, execID string, config types.ExecConfig) (types.HijackedResponse, error) {
headers := map[string][]string{"Content-Type": {"application/json"}}
return cli.postHijacked(ctx, "/exec/"+execID+"/start", nil, config, headers)
}
// ContainerExecInspect returns information about a specific exec process on the docker host.
func (cli *Client) ContainerExecInspect(ctx context.Context, execID string) (types.ContainerExecInspect, error) {
var response types.ContainerExecInspect
resp, err := cli.get(ctx, "/exec/"+execID+"/json", nil, nil)
if err != nil {
return response, err
}
err = json.NewDecoder(resp.body).Decode(&response)
ensureReaderClosed(resp)
return response, err
}

View File

@@ -0,0 +1,157 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"context"
"github.com/hyperhq/hyper-api/types"
)
func TestContainerExecCreateError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ContainerExecCreate(context.Background(), "container_id", types.ExecConfig{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestContainerExecCreate(t *testing.T) {
expectedURL := "/containers/container_id/exec"
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("expected URL '%s', got '%s'", expectedURL, req.URL)
}
if req.Method != "POST" {
return nil, fmt.Errorf("expected POST method, got %s", req.Method)
}
// FIXME validate the content is the given ExecConfig ?
if err := req.ParseForm(); err != nil {
return nil, err
}
execConfig := &types.ExecConfig{}
if err := json.NewDecoder(req.Body).Decode(execConfig); err != nil {
return nil, err
}
if execConfig.User != "user" {
return nil, fmt.Errorf("expected an execConfig with User == 'user', got %v", execConfig)
}
b, err := json.Marshal(types.ContainerExecCreateResponse{
ID: "exec_id",
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(b)),
}, nil
}),
}
r, err := client.ContainerExecCreate(context.Background(), "container_id", types.ExecConfig{
User: "user",
})
if err != nil {
t.Fatal(err)
}
if r.ID != "exec_id" {
t.Fatalf("expected `exec_id`, got %s", r.ID)
}
}
func TestContainerExecStartError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
err := client.ContainerExecStart(context.Background(), "nothing", types.ExecStartCheck{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestContainerExecStart(t *testing.T) {
expectedURL := "/exec/exec_id/start"
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
if err := req.ParseForm(); err != nil {
return nil, err
}
execStartCheck := &types.ExecStartCheck{}
if err := json.NewDecoder(req.Body).Decode(execStartCheck); err != nil {
return nil, err
}
if execStartCheck.Tty || !execStartCheck.Detach {
return nil, fmt.Errorf("expected execStartCheck{Detach:true,Tty:false}, got %v", execStartCheck)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
}, nil
}),
}
err := client.ContainerExecStart(context.Background(), "exec_id", types.ExecStartCheck{
Detach: true,
Tty: false,
})
if err != nil {
t.Fatal(err)
}
}
func TestContainerExecInspectError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ContainerExecInspect(context.Background(), "nothing")
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestContainerExecInspect(t *testing.T) {
expectedURL := "/exec/exec_id/json"
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
b, err := json.Marshal(types.ContainerExecInspect{
ExecID: "exec_id",
ContainerID: "container_id",
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(b)),
}, nil
}),
}
inspect, err := client.ContainerExecInspect(context.Background(), "exec_id")
if err != nil {
t.Fatal(err)
}
if inspect.ExecID != "exec_id" {
t.Fatalf("expected ExecID to be `exec_id`, got %s", inspect.ExecID)
}
if inspect.ContainerID != "container_id" {
t.Fatalf("expected ContainerID `container_id`, got %s", inspect.ContainerID)
}
}

View File

@@ -0,0 +1,20 @@
package client
import (
"io"
"net/url"
"context"
)
// ContainerExport retrieves the raw contents of a container
// and returns them as an io.ReadCloser. It's up to the caller
// to close the stream.
func (cli *Client) ContainerExport(ctx context.Context, containerID string) (io.ReadCloser, error) {
serverResp, err := cli.get(ctx, "/containers/"+containerID+"/export", url.Values{}, nil)
if err != nil {
return nil, err
}
return serverResp.body, nil
}

View File

@@ -0,0 +1,50 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"context"
)
func TestContainerExportError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ContainerExport(context.Background(), "nothing")
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestContainerExport(t *testing.T) {
expectedURL := "/containers/container_id/export"
client := &Client{
transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) {
if !strings.HasPrefix(r.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte("response"))),
}, nil
}),
}
body, err := client.ContainerExport(context.Background(), "container_id")
if err != nil {
t.Fatal(err)
}
defer body.Close()
content, err := ioutil.ReadAll(body)
if err != nil {
t.Fatal(err)
}
if string(content) != "response" {
t.Fatalf("expected response to contain 'response', got %s", string(content))
}
}

View File

@@ -0,0 +1,54 @@
package client
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
"context"
"github.com/hyperhq/hyper-api/types"
)
// ContainerInspect returns the container information.
func (cli *Client) ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error) {
serverResp, err := cli.get(ctx, "/containers/"+containerID+"/json", nil, nil)
if err != nil {
if serverResp.statusCode == http.StatusNotFound {
return types.ContainerJSON{}, containerNotFoundError{containerID}
}
return types.ContainerJSON{}, err
}
var response types.ContainerJSON
err = json.NewDecoder(serverResp.body).Decode(&response)
ensureReaderClosed(serverResp)
return response, err
}
// ContainerInspectWithRaw returns the container information and it's raw representation.
func (cli *Client) ContainerInspectWithRaw(ctx context.Context, containerID string, getSize bool) (types.ContainerJSON, []byte, error) {
query := url.Values{}
if getSize {
query.Set("size", "1")
}
serverResp, err := cli.get(ctx, "/containers/"+containerID+"/json", query, nil)
if err != nil {
if serverResp.statusCode == http.StatusNotFound {
return types.ContainerJSON{}, nil, containerNotFoundError{containerID}
}
return types.ContainerJSON{}, nil, err
}
defer ensureReaderClosed(serverResp)
body, err := ioutil.ReadAll(serverResp.body)
if err != nil {
return types.ContainerJSON{}, nil, err
}
var response types.ContainerJSON
rdr := bytes.NewReader(body)
err = json.NewDecoder(rdr).Decode(&response)
return response, body, err
}

View File

@@ -0,0 +1,125 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"context"
"github.com/hyperhq/hyper-api/types"
)
func TestContainerInspectError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ContainerInspect(context.Background(), "nothing")
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestContainerInspectContainerNotFound(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")),
}
_, err := client.ContainerInspect(context.Background(), "unknown")
if err == nil || !IsErrContainerNotFound(err) {
t.Fatalf("expected a containerNotFound error, got %v", err)
}
}
func TestContainerInspect(t *testing.T) {
expectedURL := "/containers/container_id/json"
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
content, err := json.Marshal(types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
ID: "container_id",
Image: "image",
Name: "name",
},
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(content)),
}, nil
}),
}
r, err := client.ContainerInspect(context.Background(), "container_id")
if err != nil {
t.Fatal(err)
}
if r.ID != "container_id" {
t.Fatalf("expected `container_id`, got %s", r.ID)
}
if r.Image != "image" {
t.Fatalf("expected `image`, got %s", r.ID)
}
if r.Name != "name" {
t.Fatalf("expected `name`, got %s", r.ID)
}
}
func TestContainerInspectNode(t *testing.T) {
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
content, err := json.Marshal(types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
ID: "container_id",
Image: "image",
Name: "name",
Node: &types.ContainerNode{
ID: "container_node_id",
Addr: "container_node",
Labels: map[string]string{"foo": "bar"},
},
},
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(content)),
}, nil
}),
}
r, err := client.ContainerInspect(context.Background(), "container_id")
if err != nil {
t.Fatal(err)
}
if r.ID != "container_id" {
t.Fatalf("expected `container_id`, got %s", r.ID)
}
if r.Image != "image" {
t.Fatalf("expected `image`, got %s", r.ID)
}
if r.Name != "name" {
t.Fatalf("expected `name`, got %s", r.ID)
}
if r.Node.ID != "container_node_id" {
t.Fatalf("expected `container_node_id`, got %s", r.Node.ID)
}
if r.Node.Addr != "container_node" {
t.Fatalf("expected `container_node`, got %s", r.Node.Addr)
}
foo, ok := r.Node.Labels["foo"]
if foo != "bar" || !ok {
t.Fatalf("expected `bar` for label `foo`")
}
}

View File

@@ -0,0 +1,16 @@
package client
import (
"context"
"net/url"
)
// ContainerKill terminates the container process but does not remove the container from the docker host.
func (cli *Client) ContainerKill(ctx context.Context, containerID, signal string) error {
query := url.Values{}
query.Set("signal", signal)
resp, err := cli.post(ctx, "/containers/"+containerID+"/kill", query, nil, nil)
ensureReaderClosed(resp)
return err
}

View File

@@ -0,0 +1,46 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"context"
)
func TestContainerKillError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
err := client.ContainerKill(context.Background(), "nothing", "SIGKILL")
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestContainerKill(t *testing.T) {
expectedURL := "/containers/container_id/kill"
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
signal := req.URL.Query().Get("signal")
if signal != "SIGKILL" {
return nil, fmt.Errorf("signal not set in URL query properly. Expected 'SIGKILL', got %s", signal)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
}, nil
}),
}
err := client.ContainerKill(context.Background(), "container_id", "SIGKILL")
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,56 @@
package client
import (
"encoding/json"
"net/url"
"strconv"
"context"
"github.com/hyperhq/hyper-api/types"
"github.com/hyperhq/hyper-api/types/filters"
)
// ContainerList returns the list of containers in the docker host.
func (cli *Client) ContainerList(ctx context.Context, options types.ContainerListOptions) ([]types.Container, error) {
query := url.Values{}
if options.All {
query.Set("all", "1")
}
if options.Limit != -1 {
query.Set("limit", strconv.Itoa(options.Limit))
}
if options.Since != "" {
query.Set("since", options.Since)
}
if options.Before != "" {
query.Set("before", options.Before)
}
if options.Size {
query.Set("size", "1")
}
if options.Filter.Len() > 0 {
filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filter)
if err != nil {
return nil, err
}
query.Set("filters", filterJSON)
}
resp, err := cli.get(ctx, "/containers/json", query, nil)
if err != nil {
return nil, err
}
var containers []types.Container
err = json.NewDecoder(resp.body).Decode(&containers)
ensureReaderClosed(resp)
return containers, err
}

View File

@@ -0,0 +1,96 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"context"
"github.com/hyperhq/hyper-api/types"
"github.com/hyperhq/hyper-api/types/filters"
)
func TestContainerListError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ContainerList(context.Background(), types.ContainerListOptions{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestContainerList(t *testing.T) {
expectedURL := "/containers/json"
expectedFilters := `{"before":{"container":true},"label":{"label1":true,"label2":true}}`
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
query := req.URL.Query()
all := query.Get("all")
if all != "1" {
return nil, fmt.Errorf("all not set in URL query properly. Expected '1', got %s", all)
}
limit := query.Get("limit")
if limit != "0" {
return nil, fmt.Errorf("limit should have not be present in query. Expected '0', got %s", limit)
}
since := query.Get("since")
if since != "container" {
return nil, fmt.Errorf("since not set in URL query properly. Expected 'container', got %s", since)
}
before := query.Get("before")
if before != "" {
return nil, fmt.Errorf("before should have not be present in query, go %s", before)
}
size := query.Get("size")
if size != "1" {
return nil, fmt.Errorf("size not set in URL query properly. Expected '1', got %s", size)
}
filters := query.Get("filters")
if filters != expectedFilters {
return nil, fmt.Errorf("expected filters incoherent '%v' with actual filters %v", expectedFilters, filters)
}
b, err := json.Marshal([]types.Container{
{
ID: "container_id1",
},
{
ID: "container_id2",
},
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(b)),
}, nil
}),
}
filters := filters.NewArgs()
filters.Add("label", "label1")
filters.Add("label", "label2")
filters.Add("before", "container")
containers, err := client.ContainerList(context.Background(), types.ContainerListOptions{
Size: true,
All: true,
Since: "container",
Filter: filters,
})
if err != nil {
t.Fatal(err)
}
if len(containers) != 2 {
t.Fatalf("expected 2 containers, got %v", containers)
}
}

View File

@@ -0,0 +1,51 @@
package client
import (
"context"
"io"
"net/url"
"time"
"github.com/hyperhq/hyper-api/types"
timetypes "github.com/hyperhq/hyper-api/types/time"
)
// ContainerLogs returns the logs generated by a container in an io.ReadCloser.
// It's up to the caller to close the stream.
func (cli *Client) ContainerLogs(ctx context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error) {
query := url.Values{}
if options.ShowStdout {
query.Set("stdout", "1")
}
if options.ShowStderr {
query.Set("stderr", "1")
}
if options.Since != "" {
ts, err := timetypes.GetTimestamp(options.Since, time.Now())
if err != nil {
return nil, err
}
query.Set("since", ts)
}
if options.Timestamps {
query.Set("timestamps", "1")
}
if options.Details {
query.Set("details", "1")
}
if options.Follow {
query.Set("follow", "1")
}
query.Set("tail", options.Tail)
resp, err := cli.get(ctx, "/containers/"+container+"/logs", query, nil)
if err != nil {
return nil, err
}
return resp.body, nil
}

View File

@@ -0,0 +1,133 @@
package client
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"testing"
"time"
"github.com/hyperhq/hyper-api/types"
"context"
)
func TestContainerLogsError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ContainerLogs(context.Background(), "container_id", types.ContainerLogsOptions{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
_, err = client.ContainerLogs(context.Background(), "container_id", types.ContainerLogsOptions{
Since: "2006-01-02TZ",
})
if err == nil || !strings.Contains(err.Error(), `parsing time "2006-01-02TZ"`) {
t.Fatalf("expected a 'parsing time' error, got %v", err)
}
}
func TestContainerLogs(t *testing.T) {
expectedURL := "/containers/container_id/logs"
cases := []struct {
options types.ContainerLogsOptions
expectedQueryParams map[string]string
}{
{
expectedQueryParams: map[string]string{
"tail": "",
},
},
{
options: types.ContainerLogsOptions{
Tail: "any",
},
expectedQueryParams: map[string]string{
"tail": "any",
},
},
{
options: types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Timestamps: true,
Details: true,
Follow: true,
},
expectedQueryParams: map[string]string{
"tail": "",
"stdout": "1",
"stderr": "1",
"timestamps": "1",
"details": "1",
"follow": "1",
},
},
{
options: types.ContainerLogsOptions{
// An complete invalid date, timestamp or go duration will be
// passed as is
Since: "invalid but valid",
},
expectedQueryParams: map[string]string{
"tail": "",
"since": "invalid but valid",
},
},
}
for _, logCase := range cases {
client := &Client{
transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) {
if !strings.HasPrefix(r.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
}
// Check query parameters
query := r.URL.Query()
for key, expected := range logCase.expectedQueryParams {
actual := query.Get(key)
if actual != expected {
return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
}
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte("response"))),
}, nil
}),
}
body, err := client.ContainerLogs(context.Background(), "container_id", logCase.options)
if err != nil {
t.Fatal(err)
}
defer body.Close()
content, err := ioutil.ReadAll(body)
if err != nil {
t.Fatal(err)
}
if string(content) != "response" {
t.Fatalf("expected response to contain 'response', got %s", string(content))
}
}
}
func ExampleClient_ContainerLogs_withTimeout() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
client, _ := NewEnvClient()
reader, err := client.ContainerLogs(ctx, "container_id", types.ContainerLogsOptions{})
if err != nil {
log.Fatal(err)
}
_, err = io.Copy(os.Stdout, reader)
if err != nil && err != io.EOF {
log.Fatal(err)
}
}

View File

@@ -0,0 +1,10 @@
package client
import "context"
// ContainerPause pauses the main process of a given container without terminating it.
func (cli *Client) ContainerPause(ctx context.Context, containerID string) error {
resp, err := cli.post(ctx, "/containers/"+containerID+"/pause", nil, nil, nil)
ensureReaderClosed(resp)
return err
}

View File

@@ -0,0 +1,41 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"context"
)
func TestContainerPauseError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
err := client.ContainerPause(context.Background(), "nothing")
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestContainerPause(t *testing.T) {
expectedURL := "/containers/container_id/pause"
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
}, nil
}),
}
err := client.ContainerPause(context.Background(), "container_id")
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,32 @@
package client
import (
"context"
"encoding/json"
"net/url"
"github.com/hyperhq/hyper-api/types"
)
// ContainerRemove kills and removes a container from the docker host.
func (cli *Client) ContainerRemove(ctx context.Context, container string, options types.ContainerRemoveOptions) ([]string, error) {
var warnings []string
query := url.Values{}
if options.RemoveVolumes {
query.Set("v", "1")
}
if options.RemoveLinks {
query.Set("link", "1")
}
if options.Force {
query.Set("force", "1")
}
resp, err := cli.delete(ctx, "/containers/"+container, query, nil)
if err == nil {
json.NewDecoder(resp.body).Decode(&warnings)
}
ensureReaderClosed(resp)
return warnings, err
}

View File

@@ -0,0 +1,59 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"context"
"github.com/hyperhq/hyper-api/types"
)
func TestContainerRemoveError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
err := client.ContainerRemove(context.Background(), "container_id", types.ContainerRemoveOptions{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestContainerRemove(t *testing.T) {
expectedURL := "/containers/container_id"
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
query := req.URL.Query()
volume := query.Get("v")
if volume != "1" {
return nil, fmt.Errorf("v (volume) not set in URL query properly. Expected '1', got %s", volume)
}
force := query.Get("force")
if force != "1" {
return nil, fmt.Errorf("force not set in URL query properly. Expected '1', got %s", force)
}
link := query.Get("link")
if link != "" {
return nil, fmt.Errorf("link should have not be present in query, go %s", link)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
}, nil
}),
}
err := client.ContainerRemove(context.Background(), "container_id", types.ContainerRemoveOptions{
RemoveVolumes: true,
Force: true,
})
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,16 @@
package client
import (
"net/url"
"context"
)
// ContainerRename changes the name of a given container.
func (cli *Client) ContainerRename(ctx context.Context, containerID, newContainerName string) error {
query := url.Values{}
query.Set("name", newContainerName)
resp, err := cli.post(ctx, "/containers/"+containerID+"/rename", query, nil, nil)
ensureReaderClosed(resp)
return err
}

View File

@@ -0,0 +1,46 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"context"
)
func TestContainerRenameError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
err := client.ContainerRename(context.Background(), "nothing", "newNothing")
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestContainerRename(t *testing.T) {
expectedURL := "/containers/container_id/rename"
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
name := req.URL.Query().Get("name")
if name != "newName" {
return nil, fmt.Errorf("name not set in URL query properly. Expected 'newName', got %s", name)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
}, nil
}),
}
err := client.ContainerRename(context.Background(), "container_id", "newName")
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,29 @@
package client
import (
"net/url"
"strconv"
"context"
"github.com/hyperhq/hyper-api/types"
)
// ContainerResize changes the size of the tty for a container.
func (cli *Client) ContainerResize(ctx context.Context, containerID string, options types.ResizeOptions) error {
return cli.resize(ctx, "/containers/"+containerID, options.Height, options.Width)
}
// ContainerExecResize changes the size of the tty for an exec process running inside a container.
func (cli *Client) ContainerExecResize(ctx context.Context, execID string, options types.ResizeOptions) error {
return cli.resize(ctx, "/exec/"+execID, options.Height, options.Width)
}
func (cli *Client) resize(ctx context.Context, basePath string, height, width int) error {
query := url.Values{}
query.Set("h", strconv.Itoa(height))
query.Set("w", strconv.Itoa(width))
resp, err := cli.post(ctx, basePath+"/resize", query, nil, nil)
ensureReaderClosed(resp)
return err
}

View File

@@ -0,0 +1,82 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"context"
"github.com/hyperhq/hyper-api/types"
)
func TestContainerResizeError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
err := client.ContainerResize(context.Background(), "container_id", types.ResizeOptions{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestContainerExecResizeError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
err := client.ContainerExecResize(context.Background(), "exec_id", types.ResizeOptions{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestContainerResize(t *testing.T) {
client := &Client{
transport: newMockClient(nil, resizeTransport("/containers/container_id/resize")),
}
err := client.ContainerResize(context.Background(), "container_id", types.ResizeOptions{
Height: 500,
Width: 600,
})
if err != nil {
t.Fatal(err)
}
}
func TestContainerExecResize(t *testing.T) {
client := &Client{
transport: newMockClient(nil, resizeTransport("/exec/exec_id/resize")),
}
err := client.ContainerExecResize(context.Background(), "exec_id", types.ResizeOptions{
Height: 500,
Width: 600,
})
if err != nil {
t.Fatal(err)
}
}
func resizeTransport(expectedURL string) func(req *http.Request) (*http.Response, error) {
return func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
query := req.URL.Query()
h := query.Get("h")
if h != "500" {
return nil, fmt.Errorf("h not set in URL query properly. Expected '500', got %s", h)
}
w := query.Get("w")
if w != "600" {
return nil, fmt.Errorf("w not set in URL query properly. Expected '600', got %s", w)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
}, nil
}
}

View File

@@ -0,0 +1,19 @@
package client
import (
"net/url"
"strconv"
"context"
)
// ContainerRestart stops and starts a container again.
// It makes the daemon to wait for the container to be up again for
// a specific amount of time, given the timeout.
func (cli *Client) ContainerRestart(ctx context.Context, containerID string, timeout int) error {
query := url.Values{}
query.Set("t", strconv.Itoa(timeout))
resp, err := cli.post(ctx, "/containers/"+containerID+"/restart", query, nil, nil)
ensureReaderClosed(resp)
return err
}

View File

@@ -0,0 +1,48 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"time"
"context"
)
func TestContainerRestartError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
timeout := 0 * time.Second
err := client.ContainerRestart(context.Background(), "nothing", &timeout)
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestContainerRestart(t *testing.T) {
expectedURL := "/containers/container_id/restart"
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
t := req.URL.Query().Get("t")
if t != "100" {
return nil, fmt.Errorf("t (timeout) not set in URL query properly. Expected '100', got %s", t)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
}, nil
}),
}
timeout := 100 * time.Second
err := client.ContainerRestart(context.Background(), "container_id", &timeout)
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,17 @@
package client
import (
"net/url"
"context"
)
// ContainerStart sends a request to the docker daemon to start a container.
func (cli *Client) ContainerStart(ctx context.Context, containerID string, checkpointID string) error {
query := url.Values{}
query.Set("checkpoint", checkpointID)
resp, err := cli.post(ctx, "/containers/"+containerID+"/start", query, nil, nil)
ensureReaderClosed(resp)
return err
}

View File

@@ -0,0 +1,58 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"context"
"github.com/hyperhq/hyper-api/types"
)
func TestContainerStartError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
err := client.ContainerStart(context.Background(), "nothing", types.ContainerStartOptions{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestContainerStart(t *testing.T) {
expectedURL := "/containers/container_id/start"
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
// we're not expecting any payload, but if one is supplied, check it is valid.
if req.Header.Get("Content-Type") == "application/json" {
var startConfig interface{}
if err := json.NewDecoder(req.Body).Decode(&startConfig); err != nil {
return nil, fmt.Errorf("Unable to parse json: %s", err)
}
}
checkpoint := req.URL.Query().Get("checkpoint")
if checkpoint != "checkpoint_id" {
return nil, fmt.Errorf("checkpoint not set in URL query properly. Expected 'checkpoint_id', got %s", checkpoint)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
}, nil
}),
}
err := client.ContainerStart(context.Background(), "container_id", types.ContainerStartOptions{CheckpointID: "checkpoint_id"})
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,24 @@
package client
import (
"io"
"net/url"
"context"
)
// ContainerStats returns near realtime stats for a given container.
// It's up to the caller to close the io.ReadCloser returned.
func (cli *Client) ContainerStats(ctx context.Context, containerID string, stream bool) (io.ReadCloser, error) {
query := url.Values{}
query.Set("stream", "0")
if stream {
query.Set("stream", "1")
}
resp, err := cli.get(ctx, "/containers/"+containerID+"/stats", query, nil)
if err != nil {
return nil, err
}
return resp.body, err
}

View File

@@ -0,0 +1,70 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"context"
)
func TestContainerStatsError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ContainerStats(context.Background(), "nothing", false)
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestContainerStats(t *testing.T) {
expectedURL := "/containers/container_id/stats"
cases := []struct {
stream bool
expectedStream string
}{
{
expectedStream: "0",
},
{
stream: true,
expectedStream: "1",
},
}
for _, c := range cases {
client := &Client{
transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) {
if !strings.HasPrefix(r.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
}
query := r.URL.Query()
stream := query.Get("stream")
if stream != c.expectedStream {
return nil, fmt.Errorf("stream not set in URL query properly. Expected '%s', got %s", c.expectedStream, stream)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte("response"))),
}, nil
}),
}
body, err := client.ContainerStats(context.Background(), "container_id", c.stream)
if err != nil {
t.Fatal(err)
}
defer body.Close()
content, err := ioutil.ReadAll(body)
if err != nil {
t.Fatal(err)
}
if string(content) != "response" {
t.Fatalf("expected response to contain 'response', got %s", string(content))
}
}
}

View File

@@ -0,0 +1,18 @@
package client
import (
"net/url"
"strconv"
"context"
)
// ContainerStop stops a container without terminating the process.
// The process is blocked until the container stops or the timeout expires.
func (cli *Client) ContainerStop(ctx context.Context, containerID string, timeout int) error {
query := url.Values{}
query.Set("t", strconv.Itoa(timeout))
resp, err := cli.post(ctx, "/containers/"+containerID+"/stop", query, nil, nil)
ensureReaderClosed(resp)
return err
}

View File

@@ -0,0 +1,48 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"time"
"context"
)
func TestContainerStopError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
timeout := 0 * time.Second
err := client.ContainerStop(context.Background(), "nothing", &timeout)
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestContainerStop(t *testing.T) {
expectedURL := "/containers/container_id/stop"
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
t := req.URL.Query().Get("t")
if t != "100" {
return nil, fmt.Errorf("t (timeout) not set in URL query properly. Expected '100', got %s", t)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
}, nil
}),
}
timeout := 100 * time.Second
err := client.ContainerStop(context.Background(), "container_id", &timeout)
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,28 @@
package client
import (
"encoding/json"
"net/url"
"strings"
"context"
"github.com/hyperhq/hyper-api/types"
)
// ContainerTop shows process information from within a container.
func (cli *Client) ContainerTop(ctx context.Context, containerID string, arguments []string) (types.ContainerProcessList, error) {
var response types.ContainerProcessList
query := url.Values{}
if len(arguments) > 0 {
query.Set("ps_args", strings.Join(arguments, " "))
}
resp, err := cli.get(ctx, "/containers/"+containerID+"/top", query, nil)
if err != nil {
return response, err
}
err = json.NewDecoder(resp.body).Decode(&response)
ensureReaderClosed(resp)
return response, err
}

View File

@@ -0,0 +1,74 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"reflect"
"strings"
"testing"
"context"
"github.com/hyperhq/hyper-api/types"
)
func TestContainerTopError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ContainerTop(context.Background(), "nothing", []string{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestContainerTop(t *testing.T) {
expectedURL := "/containers/container_id/top"
expectedProcesses := [][]string{
{"p1", "p2"},
{"p3"},
}
expectedTitles := []string{"title1", "title2"}
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
query := req.URL.Query()
args := query.Get("ps_args")
if args != "arg1 arg2" {
return nil, fmt.Errorf("args not set in URL query properly. Expected 'arg1 arg2', got %v", args)
}
b, err := json.Marshal(types.ContainerProcessList{
Processes: [][]string{
{"p1", "p2"},
{"p3"},
},
Titles: []string{"title1", "title2"},
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(b)),
}, nil
}),
}
processList, err := client.ContainerTop(context.Background(), "container_id", []string{"arg1", "arg2"})
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(expectedProcesses, processList.Processes) {
t.Fatalf("Processes: expected %v, got %v", expectedProcesses, processList.Processes)
}
if !reflect.DeepEqual(expectedTitles, processList.Titles) {
t.Fatalf("Titles: expected %v, got %v", expectedTitles, processList.Titles)
}
}

View File

@@ -0,0 +1,10 @@
package client
import "context"
// ContainerUnpause resumes the process execution within a container
func (cli *Client) ContainerUnpause(ctx context.Context, containerID string) error {
resp, err := cli.post(ctx, "/containers/"+containerID+"/unpause", nil, nil, nil)
ensureReaderClosed(resp)
return err
}

View File

@@ -0,0 +1,41 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"context"
)
func TestContainerUnpauseError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
err := client.ContainerUnpause(context.Background(), "nothing")
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestContainerUnpause(t *testing.T) {
expectedURL := "/containers/container_id/unpause"
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
}, nil
}),
}
err := client.ContainerUnpause(context.Background(), "container_id")
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,10 @@
package client
import "context"
// ContainerUpdate updates resources of a container
func (cli *Client) ContainerUpdate(ctx context.Context, containerID string, updateConfig interface{}) error {
resp, err := cli.put(ctx, "/containers/"+containerID, nil, updateConfig, nil)
ensureReaderClosed(resp)
return err
}

View File

@@ -0,0 +1,59 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"context"
"github.com/hyperhq/hyper-api/types"
"github.com/hyperhq/hyper-api/types/container"
)
func TestContainerUpdateError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ContainerUpdate(context.Background(), "nothing", container.UpdateConfig{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestContainerUpdate(t *testing.T) {
expectedURL := "/containers/container_id/update"
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
b, err := json.Marshal(types.ContainerUpdateResponse{})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(b)),
}, nil
}),
}
_, err := client.ContainerUpdate(context.Background(), "container_id", container.UpdateConfig{
Resources: container.Resources{
CPUPeriod: 1,
},
RestartPolicy: container.RestartPolicy{
Name: "always",
},
})
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,26 @@
package client
import (
"encoding/json"
"context"
"github.com/hyperhq/hyper-api/types"
)
// ContainerWait pauses execution until a container exits.
// It returns the API status code as response of its readiness.
func (cli *Client) ContainerWait(ctx context.Context, containerID string) (int, error) {
resp, err := cli.post(ctx, "/containers/"+containerID+"/wait", nil, nil, nil)
if err != nil {
return -1, err
}
defer ensureReaderClosed(resp)
var res types.ContainerWaitResponse
if err := json.NewDecoder(resp.body).Decode(&res); err != nil {
return -1, err
}
return res.StatusCode, nil
}

View File

@@ -0,0 +1,70 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
"testing"
"time"
"github.com/hyperhq/hyper-api/types"
"context"
)
func TestContainerWaitError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
code, err := client.ContainerWait(context.Background(), "nothing")
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
if code != -1 {
t.Fatalf("expected a status code equal to '-1', got %d", code)
}
}
func TestContainerWait(t *testing.T) {
expectedURL := "/containers/container_id/wait"
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
b, err := json.Marshal(types.ContainerWaitResponse{
StatusCode: 15,
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(b)),
}, nil
}),
}
code, err := client.ContainerWait(context.Background(), "container_id")
if err != nil {
t.Fatal(err)
}
if code != 15 {
t.Fatalf("expected a status code equal to '15', got %d", code)
}
}
func ExampleClient_ContainerWait_withTimeout() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
client, _ := NewEnvClient()
_, err := client.ContainerWait(ctx, "container_id")
if err != nil {
log.Fatal(err)
}
}

106
vendor/github.com/hyperhq/hyper-api/client/cron.go generated vendored Normal file
View File

@@ -0,0 +1,106 @@
package client
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
"context"
"github.com/hyperhq/hyper-api/types"
"github.com/hyperhq/hyper-api/types/filters"
)
// CronCreate creates a cron in the Hyper_.
func (cli *Client) CronCreate(ctx context.Context, name string, sv types.Cron) (types.Cron, error) {
var cron types.Cron
var r = url.Values{}
r.Set("name", name)
resp, err := cli.post(ctx, "/crons/create", r, sv, nil)
if err != nil {
return cron, err
}
err = json.NewDecoder(resp.body).Decode(&cron)
ensureReaderClosed(resp)
return cron, err
}
// CronDelete removes a cron from the Hyper_.
func (cli *Client) CronDelete(ctx context.Context, id string) error {
v := url.Values{}
resp, err := cli.delete(ctx, "/crons/"+id, v, nil)
ensureReaderClosed(resp)
return err
}
// CronList returns the crons configured in the docker host.
func (cli *Client) CronList(ctx context.Context, opts types.CronListOptions) ([]types.Cron, error) {
var crons = []types.Cron{}
query := url.Values{}
if opts.Filters.Len() > 0 {
filterJSON, err := filters.ToParamWithVersion(cli.version, opts.Filters)
if err != nil {
return crons, err
}
query.Set("filters", filterJSON)
}
resp, err := cli.get(ctx, "/crons", query, nil)
if err != nil {
return crons, err
}
err = json.NewDecoder(resp.body).Decode(&crons)
ensureReaderClosed(resp)
return crons, err
}
// CronInspect returns the information about a specific cron in the docker host.
func (cli *Client) CronInspect(ctx context.Context, cronID string) (types.Cron, error) {
cron, _, err := cli.CronInspectWithRaw(ctx, cronID)
return cron, err
}
// CronInspectWithRaw returns the information about a specific cron in the docker host and it's raw representation
func (cli *Client) CronInspectWithRaw(ctx context.Context, cronID string) (types.Cron, []byte, error) {
var cron types.Cron
resp, err := cli.get(ctx, "/crons/"+cronID, nil, nil)
if err != nil {
if resp.statusCode == http.StatusNotFound {
return cron, nil, cronNotFoundError{cronID}
}
return cron, nil, err
}
defer ensureReaderClosed(resp)
body, err := ioutil.ReadAll(resp.body)
if err != nil {
return cron, nil, err
}
rdr := bytes.NewReader(body)
err = json.NewDecoder(rdr).Decode(&cron)
return cron, body, err
}
// CronHistory
func (cli *Client) CronHistory(ctx context.Context, id, since, tail string) ([]types.Event, error) {
var (
es = []types.Event{}
v = url.Values{}
)
if since != "" {
v.Set("since", since)
}
if tail != "" {
v.Set("tail", tail)
}
resp, err := cli.get(ctx, "/crons/"+id+"/history", v, nil)
if err != nil {
return nil, err
}
defer ensureReaderClosed(resp)
err = json.NewDecoder(resp.body).Decode(&es)
return es, err
}

165
vendor/github.com/hyperhq/hyper-api/client/errors.go generated vendored Normal file
View File

@@ -0,0 +1,165 @@
package client
import (
"errors"
"fmt"
)
// ErrConnectionFailed is an error raised when the connection between the client and the server failed.
var ErrConnectionFailed = errors.New("Cannot connect to the Hyper.sh server.")
// imageNotFoundError implements an error returned when an image is not in the docker host.
type imageNotFoundError struct {
imageID string
}
// Error returns a string representation of an imageNotFoundError
func (i imageNotFoundError) Error() string {
return fmt.Sprintf("Error: No such image: %s", i.imageID)
}
// IsErrImageNotFound returns true if the error is caused
// when an image is not found in the docker host.
func IsErrImageNotFound(err error) bool {
_, ok := err.(imageNotFoundError)
return ok
}
// containerNotFoundError implements an error returned when a container is not in the docker host.
type containerNotFoundError struct {
containerID string
}
// Error returns a string representation of a containerNotFoundError
func (e containerNotFoundError) Error() string {
return fmt.Sprintf("Error: No such container: %s", e.containerID)
}
// IsErrContainerNotFound returns true if the error is caused
// when a container is not found in the docker host.
func IsErrContainerNotFound(err error) bool {
_, ok := err.(containerNotFoundError)
return ok
}
// networkNotFoundError implements an error returned when a network is not in the docker host.
type networkNotFoundError struct {
networkID string
}
// Error returns a string representation of a networkNotFoundError
func (e networkNotFoundError) Error() string {
return fmt.Sprintf("Error: No such network: %s", e.networkID)
}
// IsErrNetworkNotFound returns true if the error is caused
// when a network is not found in the docker host.
func IsErrNetworkNotFound(err error) bool {
_, ok := err.(networkNotFoundError)
return ok
}
// snapshotNotFoundError implements an error returned when a volume is not in the docker host.
type snapshotNotFoundError struct {
snapshotID string
}
// Error returns a string representation of an networkNotFoundError
func (e snapshotNotFoundError) Error() string {
return fmt.Sprintf("Error: No such snapshot: %s", e.snapshotID)
}
// volumeNotFoundError implements an error returned when a volume is not in the docker host.
type volumeNotFoundError struct {
volumeID string
}
// Error returns a string representation of a networkNotFoundError
func (e volumeNotFoundError) Error() string {
return fmt.Sprintf("Error: No such volume: %s", e.volumeID)
}
// IsErrVolumeNotFound returns true if the error is caused
// when a volume is not found in the docker host.
func IsErrVolumeNotFound(err error) bool {
_, ok := err.(volumeNotFoundError)
return ok
}
// serviceNotFoundError implements an error returned when a service is not in the docker host.
type serviceNotFoundError struct {
serviceID string
}
// Error returns a string representation of a networkNotFoundError
func (e serviceNotFoundError) Error() string {
return fmt.Sprintf("Error: No such service: %s", e.serviceID)
}
// IsErrVolumeNotFound returns true if the error is caused
// when a volume is not found in the docker host.
func IsErrServiceNotFound(err error) bool {
_, ok := err.(serviceNotFoundError)
return ok
}
// cronNotFoundError implements an error returned when a cron is not in the docker host.
type cronNotFoundError struct {
cronID string
}
// Error returns a string representation of a networkNotFoundError
func (e cronNotFoundError) Error() string {
return fmt.Sprintf("Error: No such cron job: %s", e.cronID)
}
// IsErrVolumeNotFound returns true if the error is caused
// when a volume is not found in the docker host.
func IsErrCronNotFound(err error) bool {
_, ok := err.(cronNotFoundError)
return ok
}
// funcNotFoundError implements an error returned when a func is not in the docker host.
type funcNotFoundError struct {
name string
}
// Error returns a string representation of a funcNotFoundError
func (e funcNotFoundError) Error() string {
return fmt.Sprintf("Error: No such function: %s", e.name)
}
// funcCallNotFoundError implements an error returned when a func call is not in the docker host.
type funcCallNotFoundError struct {
id string
}
// Error returns a string representation of a funcNotFoundError
func (e funcCallNotFoundError) Error() string {
return fmt.Sprintf("Error: No such call id: %s", e.id)
}
// IsErrFuncNotFound returns true if the error is caused
// when a func is not found in the docker host.
func IsErrFuncNotFound(err error) bool {
_, ok := err.(funcNotFoundError)
return ok
}
// unauthorizedError represents an authorization error in a remote registry.
type unauthorizedError struct {
cause error
}
// Error returns a string representation of an unauthorizedError
func (u unauthorizedError) Error() string {
return u.cause.Error()
}
// IsErrUnauthorized returns true if the error is caused
// when a remote registry authentication fails
func IsErrUnauthorized(err error) bool {
_, ok := err.(unauthorizedError)
return ok
}

48
vendor/github.com/hyperhq/hyper-api/client/events.go generated vendored Normal file
View File

@@ -0,0 +1,48 @@
package client
import (
"io"
"net/url"
"time"
"context"
"github.com/hyperhq/hyper-api/types"
"github.com/hyperhq/hyper-api/types/filters"
timetypes "github.com/hyperhq/hyper-api/types/time"
)
// Events returns a stream of events in the daemon in a ReadCloser.
// It's up to the caller to close the stream.
func (cli *Client) Events(ctx context.Context, options types.EventsOptions) (io.ReadCloser, error) {
query := url.Values{}
ref := time.Now()
if options.Since != "" {
ts, err := timetypes.GetTimestamp(options.Since, ref)
if err != nil {
return nil, err
}
query.Set("since", ts)
}
if options.Until != "" {
ts, err := timetypes.GetTimestamp(options.Until, ref)
if err != nil {
return nil, err
}
query.Set("until", ts)
}
if options.Filters.Len() > 0 {
filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filters)
if err != nil {
return nil, err
}
query.Set("filters", filterJSON)
}
serverResponse, err := cli.get(ctx, "/events", query, nil)
if err != nil {
return nil, err
}
return serverResponse.body, nil
}

View File

@@ -0,0 +1,126 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"context"
"github.com/hyperhq/hyper-api/types"
"github.com/hyperhq/hyper-api/types/filters"
)
func TestEventsErrorInOptions(t *testing.T) {
errorCases := []struct {
options types.EventsOptions
expectedError string
}{
{
options: types.EventsOptions{
Since: "2006-01-02TZ",
},
expectedError: `parsing time "2006-01-02TZ"`,
},
{
options: types.EventsOptions{
Until: "2006-01-02TZ",
},
expectedError: `parsing time "2006-01-02TZ"`,
},
}
for _, e := range errorCases {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.Events(context.Background(), e.options)
if err == nil || !strings.Contains(err.Error(), e.expectedError) {
t.Fatalf("expected a error %q, got %v", e.expectedError, err)
}
}
}
func TestEventsErrorFromServer(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.Events(context.Background(), types.EventsOptions{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestEvents(t *testing.T) {
expectedURL := "/events"
filters := filters.NewArgs()
filters.Add("label", "label1")
filters.Add("label", "label2")
expectedFiltersJSON := `{"label":{"label1":true,"label2":true}}`
eventsCases := []struct {
options types.EventsOptions
expectedQueryParams map[string]string
}{
{
options: types.EventsOptions{
Since: "invalid but valid",
},
expectedQueryParams: map[string]string{
"since": "invalid but valid",
},
},
{
options: types.EventsOptions{
Until: "invalid but valid",
},
expectedQueryParams: map[string]string{
"until": "invalid but valid",
},
},
{
options: types.EventsOptions{
Filters: filters,
},
expectedQueryParams: map[string]string{
"filters": expectedFiltersJSON,
},
},
}
for _, eventsCase := range eventsCases {
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
query := req.URL.Query()
for key, expected := range eventsCase.expectedQueryParams {
actual := query.Get(key)
if actual != expected {
return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
}
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte("response"))),
}, nil
}),
}
body, err := client.Events(context.Background(), eventsCase.options)
if err != nil {
t.Fatal(err)
}
defer body.Close()
content, err := ioutil.ReadAll(body)
if err != nil {
t.Fatal(err)
}
if string(content) != "response" {
t.Fatalf("expected response to contain 'response', got %s", string(content))
}
}
}

90
vendor/github.com/hyperhq/hyper-api/client/fip.go generated vendored Normal file
View File

@@ -0,0 +1,90 @@
package client
import (
"context"
"encoding/json"
"net/url"
"github.com/hyperhq/hyper-api/types"
"github.com/hyperhq/hyper-api/types/filters"
)
func (cli *Client) FipAllocate(ctx context.Context, count string) ([]string, error) {
var result []string
var v = url.Values{}
v.Set("count", count)
serverResp, err := cli.post(ctx, "/fips/allocate", v, nil, nil)
if err != nil {
return result, err
}
json.NewDecoder(serverResp.body).Decode(&result)
ensureReaderClosed(serverResp)
return result, err
}
func (cli *Client) FipRelease(ctx context.Context, ip string) error {
var v = url.Values{}
v.Set("ip", ip)
_, err := cli.post(ctx, "/fips/release", v, nil, nil)
if err != nil {
return err
}
return nil
}
func (cli *Client) FipAttach(ctx context.Context, ip, container string) error {
var v = url.Values{}
v.Set("ip", ip)
v.Set("container", container)
_, err := cli.post(ctx, "/fips/attach", v, nil, nil)
if err != nil {
return err
}
return nil
}
func (cli *Client) FipDetach(ctx context.Context, container string) (string, error) {
var result string
var v = url.Values{}
v.Set("container", container)
resp, err := cli.post(ctx, "/fips/detach", v, nil, nil)
if err != nil {
return "", err
}
json.NewDecoder(resp.body).Decode(&result)
ensureReaderClosed(resp)
return result, nil
}
func (cli *Client) FipList(ctx context.Context, options types.NetworkListOptions) ([]map[string]string, error) {
query := url.Values{}
if options.Filters.Len() > 0 {
filterJSON, err := filters.ToParam(options.Filters)
if err != nil {
return nil, err
}
query.Set("filters", filterJSON)
}
var fips []map[string]string
resp, err := cli.get(ctx, "/fips", query, nil)
if err != nil {
return fips, err
}
err = json.NewDecoder(resp.body).Decode(&fips)
ensureReaderClosed(resp)
return fips, err
}
func (cli *Client) FipName(ctx context.Context, ip, name string) error {
var v = url.Values{}
v.Set("ip", ip)
v.Set("name", name)
resp, err := cli.post(ctx, "/fips/name", v, nil, nil)
if err != nil {
return err
}
ensureReaderClosed(resp)
return nil
}

283
vendor/github.com/hyperhq/hyper-api/client/func.go generated vendored Normal file
View File

@@ -0,0 +1,283 @@
package client
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"path"
"strconv"
"github.com/hyperhq/hyper-api/types"
"github.com/hyperhq/hyper-api/types/filters"
)
func newFuncEndpointRequest(region, method, subpath string, query url.Values, body io.Reader) (*http.Request, error) {
endpoint := os.Getenv("HYPER_FUNC_ENDPOINT")
if endpoint == "" {
endpoint = region + ".hyperfunc.io"
}
apiURL, err := url.Parse(endpoint)
if err != nil {
return nil, err
}
apiURL.Scheme = "https"
apiURL.Path = path.Join(apiURL.Path, subpath)
queryStr := query.Encode()
if queryStr != "" {
apiURL.RawQuery = queryStr
}
req, err := http.NewRequest(method, apiURL.String(), body)
if err != nil {
return nil, err
}
return req, nil
}
func funcEndpointRequestHijack(req *http.Request) (net.Conn, error) {
req.Header.Set("Connection", "Upgrade")
req.Header.Set("Upgrade", "tcp")
conn, err := tls.Dial("tcp", req.URL.Host+":443", &tls.Config{})
if err != nil {
return nil, err
}
clientConn := httputil.NewClientConn(conn, nil)
resp, err := clientConn.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != 101 {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return nil, fmt.Errorf("Error response from server: %s", bytes.TrimSpace(body))
}
respConn, _ := clientConn.Hijack()
return respConn, nil
}
func funcEndpointRequest(req *http.Request) (*http.Response, error) {
client := &http.Client{Transport: &http.Transport{
TLSClientConfig: &tls.Config{},
}}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
status := resp.StatusCode
if status < 200 || status >= 400 {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return nil, fmt.Errorf("Error response from server: %s", bytes.TrimSpace(body))
}
return resp, nil
}
func (cli *Client) FuncCreate(ctx context.Context, opts types.Func) (types.Func, error) {
var fn types.Func
_, _, err := cli.ImageInspectWithRaw(context.Background(), opts.Config.Image, false)
if err != nil {
return fn, err
}
resp, err := cli.post(ctx, "/funcs/create", nil, opts, nil)
if err != nil {
return fn, err
}
err = json.NewDecoder(resp.body).Decode(&fn)
ensureReaderClosed(resp)
return fn, err
}
func (cli *Client) FuncUpdate(ctx context.Context, name string, opts types.Func) (types.Func, error) {
var fn types.Func
resp, err := cli.put(ctx, "/funcs/"+name, nil, opts, nil)
if err != nil {
return fn, err
}
err = json.NewDecoder(resp.body).Decode(&fn)
ensureReaderClosed(resp)
return fn, err
}
func (cli *Client) FuncDelete(ctx context.Context, name string) error {
resp, err := cli.delete(ctx, "/funcs/"+name, nil, nil)
ensureReaderClosed(resp)
return err
}
func (cli *Client) FuncList(ctx context.Context, opts types.FuncListOptions) ([]types.Func, error) {
var fns = []types.Func{}
query := url.Values{}
if opts.Filters.Len() > 0 {
filterJSON, err := filters.ToParamWithVersion(cli.version, opts.Filters)
if err != nil {
return fns, err
}
query.Set("filters", filterJSON)
}
resp, err := cli.get(ctx, "/funcs", query, nil)
if err != nil {
return fns, err
}
err = json.NewDecoder(resp.body).Decode(&fns)
ensureReaderClosed(resp)
return fns, err
}
func (cli *Client) FuncInspect(ctx context.Context, name string) (types.Func, error) {
fn, _, err := cli.FuncInspectWithRaw(ctx, name)
return fn, err
}
func (cli *Client) FuncInspectWithRaw(ctx context.Context, name string) (types.Func, []byte, error) {
var fn types.Func
resp, err := cli.get(ctx, "/funcs/"+name, nil, nil)
if err != nil {
if resp.statusCode == http.StatusNotFound {
return fn, nil, funcNotFoundError{name}
}
return fn, nil, err
}
defer ensureReaderClosed(resp)
body, err := ioutil.ReadAll(resp.body)
if err != nil {
return fn, nil, err
}
rdr := bytes.NewReader(body)
err = json.NewDecoder(rdr).Decode(&fn)
return fn, body, err
}
func (cli *Client) FuncInspectWithCallId(ctx context.Context, id string) (*types.Func, error) {
var fn types.Func
resp, err := cli.get(ctx, "/funcs/call/"+id, nil, nil)
if err != nil {
if resp.statusCode == http.StatusNotFound {
return nil, funcCallNotFoundError{id}
}
return nil, err
}
defer ensureReaderClosed(resp)
body, err := ioutil.ReadAll(resp.body)
if err != nil {
return nil, err
}
rdr := bytes.NewReader(body)
err = json.NewDecoder(rdr).Decode(&fn)
return &fn, err
}
func (cli *Client) FuncCall(ctx context.Context, region, name string, stdin io.Reader, sync bool) (io.ReadCloser, error) {
fn, _, err := cli.FuncInspectWithRaw(ctx, name)
if err != nil {
return nil, err
}
subpath := ""
if sync {
subpath += "/sync"
}
req, err := newFuncEndpointRequest(region, "POST", path.Join("call", name, fn.UUID, subpath), nil, stdin)
if err != nil {
return nil, err
}
resp, err := funcEndpointRequest(req)
if err != nil {
return nil, err
}
return resp.Body, nil
}
func (cli *Client) FuncGet(ctx context.Context, region, callId string, wait bool) (io.ReadCloser, error) {
fn, err := cli.FuncInspectWithCallId(ctx, callId)
if err != nil {
return nil, err
}
subpath := callId
if wait {
subpath += "/wait"
}
req, err := newFuncEndpointRequest(region, "GET", path.Join("output", fn.Name, fn.UUID, subpath), nil, nil)
if err != nil {
return nil, err
}
resp, err := funcEndpointRequest(req)
if err != nil {
return nil, err
}
return resp.Body, nil
}
func (cli *Client) FuncLogs(ctx context.Context, region, name, callId string, follow bool, tail string) (io.ReadCloser, error) {
fn, _, err := cli.FuncInspectWithRaw(ctx, name)
if err != nil {
return nil, err
}
query := url.Values{}
if callId != "" {
query.Set("callid", callId)
}
if follow {
query.Add("follow", strconv.FormatBool(follow))
}
if tail != "" {
query.Add("tail", tail)
}
req, err := newFuncEndpointRequest(region, "GET", path.Join("logs", name, fn.UUID, ""), query, nil)
if err != nil {
return nil, err
}
if follow {
conn, err := funcEndpointRequestHijack(req)
if err != nil {
return nil, err
}
return conn.(io.ReadCloser), nil
}
resp, err := funcEndpointRequest(req)
if err != nil {
return nil, err
}
return resp.Body, nil
}
func (cli *Client) FuncStatus(ctx context.Context, region, name string) (*types.FuncStatusResponse, error) {
fn, _, err := cli.FuncInspectWithRaw(ctx, name)
if err != nil {
return nil, err
}
query := url.Values{}
query.Set("list", strconv.FormatBool(false))
req, err := newFuncEndpointRequest(region, "GET", path.Join("status", name, fn.UUID), query, nil)
if err != nil {
return nil, err
}
resp, err := funcEndpointRequest(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var ret types.FuncStatusResponse
err = json.NewDecoder(resp.Body).Decode(&ret)
if err != nil {
return nil, err
}
return &ret, nil
}

176
vendor/github.com/hyperhq/hyper-api/client/hijack.go generated vendored Normal file
View File

@@ -0,0 +1,176 @@
package client
import (
"context"
"crypto/tls"
"errors"
"net"
"net/http/httputil"
"net/url"
"strings"
"time"
"github.com/docker/go-connections/sockets"
"github.com/hyperhq/hyper-api/signature"
"github.com/hyperhq/hyper-api/types"
)
// tlsClientCon holds tls information and a dialed connection.
type tlsClientCon struct {
*tls.Conn
rawConn net.Conn
}
func (c *tlsClientCon) CloseWrite() error {
// Go standard tls.Conn doesn't provide the CloseWrite() method so we do it
// on its underlying connection.
if conn, ok := c.rawConn.(types.CloseWriter); ok {
return conn.CloseWrite()
}
return nil
}
// postHijacked sends a POST request and hijacks the connection.
func (cli *Client) postHijacked(ctx context.Context, path string, query url.Values, body interface{}, headers map[string][]string) (types.HijackedResponse, error) {
bodyEncoded, err := encodeData(body)
if err != nil {
return types.HijackedResponse{}, err
}
req, err := cli.newRequest("POST", path, query, bodyEncoded, headers)
if err != nil {
return types.HijackedResponse{}, err
}
req.URL.Host = cli.addr
req.Header.Set("Connection", "Upgrade")
req.Header.Set("Upgrade", "tcp")
req = signature.Sign4(cli.accessKey, cli.secretKey, req, cli.region)
conn, err := dial(cli.proto, cli.addr, cli.transport.TLSConfig())
if err != nil {
if strings.Contains(err.Error(), "connection refused") {
return types.HijackedResponse{}, ErrConnectionFailed
}
return types.HijackedResponse{}, err
}
// When we set up a TCP connection for hijack, there could be long periods
// of inactivity (a long running command with no output) that in certain
// network setups may cause ECONNTIMEOUT, leaving the client in an unknown
// state. Setting TCP KeepAlive on the socket connection will prohibit
// ECONNTIMEOUT unless the socket connection truly is broken
if tcpConn, ok := conn.(*net.TCPConn); ok {
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(30 * time.Second)
}
clientconn := httputil.NewClientConn(conn, nil)
defer clientconn.Close()
// Server hijacks the connection, error 'connection closed' expected
resp, err := clientconn.Do(req)
rwc, br := clientconn.Hijack()
return types.HijackedResponse{Conn: rwc, Reader: br, Resp: resp}, err
}
func tlsDial(network, addr string, config *tls.Config) (net.Conn, error) {
return tlsDialWithDialer(new(net.Dialer), network, addr, config)
}
// We need to copy Go's implementation of tls.Dial (pkg/cryptor/tls/tls.go) in
// order to return our custom tlsClientCon struct which holds both the tls.Conn
// object _and_ its underlying raw connection. The rationale for this is that
// we need to be able to close the write end of the connection when attaching,
// which tls.Conn does not provide.
func tlsDialWithDialer(dialer *net.Dialer, network, addr string, config *tls.Config) (net.Conn, error) {
// We want the Timeout and Deadline values from dialer to cover the
// whole process: TCP connection and TLS handshake. This means that we
// also need to start our own timers now.
timeout := dialer.Timeout
if !dialer.Deadline.IsZero() {
deadlineTimeout := dialer.Deadline.Sub(time.Now())
if timeout == 0 || deadlineTimeout < timeout {
timeout = deadlineTimeout
}
}
var errChannel chan error
if timeout != 0 {
errChannel = make(chan error, 2)
time.AfterFunc(timeout, func() {
errChannel <- errors.New("")
})
}
proxyDialer, err := sockets.DialerFromEnvironment(dialer)
if err != nil {
return nil, err
}
rawConn, err := proxyDialer.Dial(network, addr)
if err != nil {
return nil, err
}
// When we set up a TCP connection for hijack, there could be long periods
// of inactivity (a long running command with no output) that in certain
// network setups may cause ECONNTIMEOUT, leaving the client in an unknown
// state. Setting TCP KeepAlive on the socket connection will prohibit
// ECONNTIMEOUT unless the socket connection truly is broken
if tcpConn, ok := rawConn.(*net.TCPConn); ok {
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(30 * time.Second)
}
colonPos := strings.LastIndex(addr, ":")
if colonPos == -1 {
colonPos = len(addr)
}
hostname := addr[:colonPos]
// If no ServerName is set, infer the ServerName
// from the hostname we're connecting to.
if config.ServerName == "" {
// Make a copy to avoid polluting argument or default.
c := *config
c.ServerName = hostname
config = &c
}
conn := tls.Client(rawConn, config)
if timeout == 0 {
err = conn.Handshake()
} else {
go func() {
errChannel <- conn.Handshake()
}()
err = <-errChannel
}
if err != nil {
rawConn.Close()
return nil, err
}
// This is Docker difference with standard's crypto/tls package: returned a
// wrapper which holds both the TLS and raw connections.
return &tlsClientCon{conn, rawConn}, nil
}
func dial(proto, addr string, tlsConfig *tls.Config) (net.Conn, error) {
if tlsConfig != nil && proto != "unix" && proto != "npipe" {
// Notice this isn't Go standard's tls.Dial function
return tlsDial(proto, addr, tlsConfig)
}
if proto == "npipe" {
return sockets.DialPipe(addr, 32*time.Second)
}
return net.Dial(proto, addr)
}

View File

@@ -0,0 +1,119 @@
package client
import (
"encoding/base64"
"encoding/json"
"io"
"net/http"
"net/url"
"regexp"
"strconv"
"context"
"github.com/hyperhq/hyper-api/types"
"github.com/hyperhq/hyper-api/types/container"
)
var headerRegexp = regexp.MustCompile(`\ADocker/.+\s\((.+)\)\z`)
// ImageBuild sends request to the daemon to build images.
// The Body in the response implement an io.ReadCloser and it's up to the caller to
// close it.
func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
query, err := imageBuildOptionsToQuery(options)
if err != nil {
return types.ImageBuildResponse{}, err
}
headers := http.Header(make(map[string][]string))
buf, err := json.Marshal(options.AuthConfigs)
if err != nil {
return types.ImageBuildResponse{}, err
}
headers.Add("X-Registry-Config", base64.URLEncoding.EncodeToString(buf))
headers.Set("Content-Type", "application/tar")
serverResp, err := cli.postRaw(ctx, "/build", query, buildContext, headers)
if err != nil {
return types.ImageBuildResponse{}, err
}
osType := getDockerOS(serverResp.header.Get("Server"))
return types.ImageBuildResponse{
Body: serverResp.body,
OSType: osType,
}, nil
}
func imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, error) {
query := url.Values{
"t": options.Tags,
}
if options.SuppressOutput {
query.Set("q", "1")
}
if options.RemoteContext != "" {
query.Set("remote", options.RemoteContext)
}
if options.NoCache {
query.Set("nocache", "1")
}
if options.Remove {
query.Set("rm", "1")
} else {
query.Set("rm", "0")
}
if options.ForceRemove {
query.Set("forcerm", "1")
}
if options.PullParent {
query.Set("pull", "1")
}
if !container.Isolation.IsDefault(options.Isolation) {
query.Set("isolation", string(options.Isolation))
}
query.Set("cpusetcpus", options.CPUSetCPUs)
query.Set("cpusetmems", options.CPUSetMems)
query.Set("cpushares", strconv.FormatInt(options.CPUShares, 10))
query.Set("cpuquota", strconv.FormatInt(options.CPUQuota, 10))
query.Set("cpuperiod", strconv.FormatInt(options.CPUPeriod, 10))
query.Set("memory", strconv.FormatInt(options.Memory, 10))
query.Set("memswap", strconv.FormatInt(options.MemorySwap, 10))
query.Set("cgroupparent", options.CgroupParent)
query.Set("shmsize", strconv.FormatInt(options.ShmSize, 10))
query.Set("dockerfile", options.Dockerfile)
ulimitsJSON, err := json.Marshal(options.Ulimits)
if err != nil {
return query, err
}
query.Set("ulimits", string(ulimitsJSON))
buildArgsJSON, err := json.Marshal(options.BuildArgs)
if err != nil {
return query, err
}
query.Set("buildargs", string(buildArgsJSON))
labelsJSON, err := json.Marshal(options.Labels)
if err != nil {
return query, err
}
query.Set("labels", string(labelsJSON))
return query, nil
}
func getDockerOS(serverHeader string) string {
var osType string
matches := headerRegexp.FindStringSubmatch(serverHeader)
if len(matches) > 0 {
osType = matches[1]
}
return osType
}

View File

@@ -0,0 +1,230 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"reflect"
"strings"
"testing"
"context"
"github.com/docker/go-units"
"github.com/hyperhq/hyper-api/types"
"github.com/hyperhq/hyper-api/types/container"
)
func TestImageBuildError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ImageBuild(context.Background(), nil, types.ImageBuildOptions{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestImageBuild(t *testing.T) {
emptyRegistryConfig := "bnVsbA=="
buildCases := []struct {
buildOptions types.ImageBuildOptions
expectedQueryParams map[string]string
expectedTags []string
expectedRegistryConfig string
}{
{
buildOptions: types.ImageBuildOptions{
SuppressOutput: true,
NoCache: true,
Remove: true,
ForceRemove: true,
PullParent: true,
},
expectedQueryParams: map[string]string{
"q": "1",
"nocache": "1",
"rm": "1",
"forcerm": "1",
"pull": "1",
},
expectedTags: []string{},
expectedRegistryConfig: emptyRegistryConfig,
},
{
buildOptions: types.ImageBuildOptions{
SuppressOutput: false,
NoCache: false,
Remove: false,
ForceRemove: false,
PullParent: false,
},
expectedQueryParams: map[string]string{
"q": "",
"nocache": "",
"rm": "0",
"forcerm": "",
"pull": "",
},
expectedTags: []string{},
expectedRegistryConfig: emptyRegistryConfig,
},
{
buildOptions: types.ImageBuildOptions{
RemoteContext: "remoteContext",
Isolation: container.Isolation("isolation"),
CPUSetCPUs: "2",
CPUSetMems: "12",
CPUShares: 20,
CPUQuota: 10,
CPUPeriod: 30,
Memory: 256,
MemorySwap: 512,
ShmSize: 10,
CgroupParent: "cgroup_parent",
Dockerfile: "Dockerfile",
},
expectedQueryParams: map[string]string{
"remote": "remoteContext",
"isolation": "isolation",
"cpusetcpus": "2",
"cpusetmems": "12",
"cpushares": "20",
"cpuquota": "10",
"cpuperiod": "30",
"memory": "256",
"memswap": "512",
"shmsize": "10",
"cgroupparent": "cgroup_parent",
"dockerfile": "Dockerfile",
"rm": "0",
},
expectedTags: []string{},
expectedRegistryConfig: emptyRegistryConfig,
},
{
buildOptions: types.ImageBuildOptions{
BuildArgs: map[string]string{
"ARG1": "value1",
"ARG2": "value2",
},
},
expectedQueryParams: map[string]string{
"buildargs": `{"ARG1":"value1","ARG2":"value2"}`,
"rm": "0",
},
expectedTags: []string{},
expectedRegistryConfig: emptyRegistryConfig,
},
{
buildOptions: types.ImageBuildOptions{
Ulimits: []*units.Ulimit{
{
Name: "nproc",
Hard: 65557,
Soft: 65557,
},
{
Name: "nofile",
Hard: 20000,
Soft: 40000,
},
},
},
expectedQueryParams: map[string]string{
"ulimits": `[{"Name":"nproc","Hard":65557,"Soft":65557},{"Name":"nofile","Hard":20000,"Soft":40000}]`,
"rm": "0",
},
expectedTags: []string{},
expectedRegistryConfig: emptyRegistryConfig,
},
{
buildOptions: types.ImageBuildOptions{
AuthConfigs: map[string]types.AuthConfig{
"https://index.docker.io/v1/": {
Auth: "dG90bwo=",
},
},
},
expectedQueryParams: map[string]string{
"rm": "0",
},
expectedTags: []string{},
expectedRegistryConfig: "eyJodHRwczovL2luZGV4LmRvY2tlci5pby92MS8iOnsiYXV0aCI6ImRHOTBid289In19",
},
}
for _, buildCase := range buildCases {
expectedURL := "/build"
client := &Client{
transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) {
if !strings.HasPrefix(r.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
}
// Check request headers
registryConfig := r.Header.Get("X-Registry-Config")
if registryConfig != buildCase.expectedRegistryConfig {
return nil, fmt.Errorf("X-Registry-Config header not properly set in the request. Expected '%s', got %s", buildCase.expectedRegistryConfig, registryConfig)
}
contentType := r.Header.Get("Content-Type")
if contentType != "application/tar" {
return nil, fmt.Errorf("Content-type header not properly set in the request. Expected 'application/tar', got %s", contentType)
}
// Check query parameters
query := r.URL.Query()
for key, expected := range buildCase.expectedQueryParams {
actual := query.Get(key)
if actual != expected {
return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
}
}
// Check tags
if len(buildCase.expectedTags) > 0 {
tags := query["t"]
if !reflect.DeepEqual(tags, buildCase.expectedTags) {
return nil, fmt.Errorf("t (tags) not set in URL query properly. Expected '%s', got %s", buildCase.expectedTags, tags)
}
}
headers := http.Header{}
headers.Add("Server", "Docker/v1.23 (MyOS)")
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte("body"))),
Header: headers,
}, nil
}),
}
buildResponse, err := client.ImageBuild(context.Background(), nil, buildCase.buildOptions)
if err != nil {
t.Fatal(err)
}
if buildResponse.OSType != "MyOS" {
t.Fatalf("expected OSType to be 'MyOS', got %s", buildResponse.OSType)
}
response, err := ioutil.ReadAll(buildResponse.Body)
if err != nil {
t.Fatal(err)
}
buildResponse.Body.Close()
if string(response) != "body" {
t.Fatalf("expected Body to contain 'body' string, got %s", response)
}
}
}
func TestGetDockerOS(t *testing.T) {
cases := map[string]string{
"Docker/v1.22 (linux)": "linux",
"Docker/v1.22 (windows)": "windows",
"Foo/v1.22 (bar)": "",
}
for header, os := range cases {
g := getDockerOS(header)
if g != os {
t.Fatalf("Expected %s, got %s", os, g)
}
}
}

View File

@@ -0,0 +1,34 @@
package client
import (
"io"
"net/url"
"context"
"github.com/hyperhq/hyper-api/types"
"github.com/hyperhq/hyper-api/types/reference"
)
// ImageCreate creates a new image based in the parent options.
// It returns the JSON content in the response body.
func (cli *Client) ImageCreate(ctx context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
repository, tag, err := reference.Parse(parentReference)
if err != nil {
return nil, err
}
query := url.Values{}
query.Set("fromImage", repository)
query.Set("tag", tag)
resp, err := cli.tryImageCreate(ctx, query, options.RegistryAuth)
if err != nil {
return nil, err
}
return resp.body, nil
}
func (cli *Client) tryImageCreate(ctx context.Context, query url.Values, registryAuth string) (*serverResponse, error) {
headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
return cli.post(ctx, "/images/create", query, nil, headers)
}

View File

@@ -0,0 +1,76 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"context"
"github.com/hyperhq/hyper-api/types"
)
func TestImageCreateError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ImageCreate(context.Background(), "reference", types.ImageCreateOptions{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server error, got %v", err)
}
}
func TestImageCreate(t *testing.T) {
expectedURL := "/images/create"
expectedImage := "test:5000/my_image"
expectedTag := "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
expectedReference := fmt.Sprintf("%s@%s", expectedImage, expectedTag)
expectedRegistryAuth := "eyJodHRwczovL2luZGV4LmRvY2tlci5pby92MS8iOnsiYXV0aCI6ImRHOTBid289IiwiZW1haWwiOiJqb2huQGRvZS5jb20ifX0="
client := &Client{
transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) {
if !strings.HasPrefix(r.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
}
registryAuth := r.Header.Get("X-Registry-Auth")
if registryAuth != expectedRegistryAuth {
return nil, fmt.Errorf("X-Registry-Auth header not properly set in the request. Expected '%s', got %s", expectedRegistryAuth, registryAuth)
}
query := r.URL.Query()
fromImage := query.Get("fromImage")
if fromImage != expectedImage {
return nil, fmt.Errorf("fromImage not set in URL query properly. Expected '%s', got %s", expectedImage, fromImage)
}
tag := query.Get("tag")
if tag != expectedTag {
return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", expectedTag, tag)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte("body"))),
}, nil
}),
}
createResponse, err := client.ImageCreate(context.Background(), expectedReference, types.ImageCreateOptions{
RegistryAuth: expectedRegistryAuth,
})
if err != nil {
t.Fatal(err)
}
response, err := ioutil.ReadAll(createResponse)
if err != nil {
t.Fatal(err)
}
if err = createResponse.Close(); err != nil {
t.Fatal(err)
}
if string(response) != "body" {
t.Fatalf("expected Body to contain 'body' string, got %s", response)
}
}

View File

@@ -0,0 +1,22 @@
package client
import (
"encoding/json"
"net/url"
"context"
"github.com/hyperhq/hyper-api/types"
)
// ImageHistory returns the changes in an image in history format.
func (cli *Client) ImageHistory(ctx context.Context, imageID string) ([]types.ImageHistory, error) {
var history []types.ImageHistory
serverResp, err := cli.get(ctx, "/images/"+imageID+"/history", url.Values{}, nil)
if err != nil {
return history, err
}
err = json.NewDecoder(serverResp.body).Decode(&history)
ensureReaderClosed(serverResp)
return history, err
}

View File

@@ -0,0 +1,60 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"context"
"github.com/hyperhq/hyper-api/types"
)
func TestImageHistoryError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ImageHistory(context.Background(), "nothing")
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server error, got %v", err)
}
}
func TestImageHistory(t *testing.T) {
expectedURL := "/images/image_id/history"
client := &Client{
transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) {
if !strings.HasPrefix(r.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
}
b, err := json.Marshal([]types.ImageHistory{
{
ID: "image_id1",
Tags: []string{"tag1", "tag2"},
},
{
ID: "image_id2",
Tags: []string{"tag1", "tag2"},
},
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(b)),
}, nil
}),
}
imageHistories, err := client.ImageHistory(context.Background(), "image_id")
if err != nil {
t.Fatal(err)
}
if len(imageHistories) != 2 {
t.Fatalf("expected 2 containers, got %v", imageHistories)
}
}

View File

@@ -0,0 +1,37 @@
package client
import (
"io"
"net/url"
"context"
"github.com/docker/distribution/reference"
"github.com/hyperhq/hyper-api/types"
)
// ImageImport creates a new image based in the source options.
// It returns the JSON content in the response body.
func (cli *Client) ImageImport(ctx context.Context, source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
if ref != "" {
//Check if the given image name can be resolved
if _, err := reference.ParseNamed(ref); err != nil {
return nil, err
}
}
query := url.Values{}
query.Set("fromSrc", source.SourceName)
query.Set("repo", ref)
query.Set("tag", options.Tag)
query.Set("message", options.Message)
for _, change := range options.Changes {
query.Add("changes", change)
}
resp, err := cli.postRaw(ctx, "/images/create", query, source.Source, nil)
if err != nil {
return nil, err
}
return resp.body, nil
}

View File

@@ -0,0 +1,81 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"reflect"
"strings"
"testing"
"context"
"github.com/hyperhq/hyper-api/types"
)
func TestImageImportError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ImageImport(context.Background(), types.ImageImportSource{}, "image:tag", types.ImageImportOptions{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server error, got %v", err)
}
}
func TestImageImport(t *testing.T) {
expectedURL := "/images/create"
client := &Client{
transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) {
if !strings.HasPrefix(r.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
}
query := r.URL.Query()
fromSrc := query.Get("fromSrc")
if fromSrc != "image_source" {
return nil, fmt.Errorf("fromSrc not set in URL query properly. Expected 'image_source', got %s", fromSrc)
}
repo := query.Get("repo")
if repo != "repository_name:imported" {
return nil, fmt.Errorf("repo not set in URL query properly. Expected 'repository_name', got %s", repo)
}
tag := query.Get("tag")
if tag != "imported" {
return nil, fmt.Errorf("tag not set in URL query properly. Expected 'imported', got %s", tag)
}
message := query.Get("message")
if message != "A message" {
return nil, fmt.Errorf("message not set in URL query properly. Expected 'A message', got %s", message)
}
changes := query["changes"]
expectedChanges := []string{"change1", "change2"}
if !reflect.DeepEqual(expectedChanges, changes) {
return nil, fmt.Errorf("changes not set in URL query properly. Expected %v, got %v", expectedChanges, changes)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte("response"))),
}, nil
}),
}
importResponse, err := client.ImageImport(context.Background(), types.ImageImportSource{
Source: strings.NewReader("source"),
SourceName: "image_source",
}, "repository_name:imported", types.ImageImportOptions{
Tag: "imported",
Message: "A message",
Changes: []string{"change1", "change2"},
})
if err != nil {
t.Fatal(err)
}
response, err := ioutil.ReadAll(importResponse)
if err != nil {
t.Fatal(err)
}
importResponse.Close()
if string(response) != "response" {
t.Fatalf("expected response to contain 'response', got %s", string(response))
}
}

View File

@@ -0,0 +1,38 @@
package client
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
"context"
"github.com/hyperhq/hyper-api/types"
)
// ImageInspectWithRaw returns the image information and its raw representation.
func (cli *Client) ImageInspectWithRaw(ctx context.Context, imageID string, getSize bool) (types.ImageInspect, []byte, error) {
query := url.Values{}
if getSize {
query.Set("size", "1")
}
serverResp, err := cli.get(ctx, "/images/"+imageID+"/json", query, nil)
if err != nil {
if serverResp.statusCode == http.StatusNotFound {
return types.ImageInspect{}, nil, imageNotFoundError{imageID}
}
return types.ImageInspect{}, nil, err
}
defer ensureReaderClosed(serverResp)
body, err := ioutil.ReadAll(serverResp.body)
if err != nil {
return types.ImageInspect{}, nil, err
}
var response types.ImageInspect
rdr := bytes.NewReader(body)
err = json.NewDecoder(rdr).Decode(&response)
return response, body, err
}

View File

@@ -0,0 +1,71 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"reflect"
"strings"
"testing"
"context"
"github.com/hyperhq/hyper-api/types"
)
func TestImageInspectError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, _, err := client.ImageInspectWithRaw(context.Background(), "nothing")
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestImageInspectImageNotFound(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusNotFound, "Server error")),
}
_, _, err := client.ImageInspectWithRaw(context.Background(), "unknown")
if err == nil || !IsErrImageNotFound(err) {
t.Fatalf("expected an imageNotFound error, got %v", err)
}
}
func TestImageInspect(t *testing.T) {
expectedURL := "/images/image_id/json"
expectedTags := []string{"tag1", "tag2"}
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
content, err := json.Marshal(types.ImageInspect{
ID: "image_id",
RepoTags: expectedTags,
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(content)),
}, nil
}),
}
imageInspect, _, err := client.ImageInspectWithRaw(context.Background(), "image_id")
if err != nil {
t.Fatal(err)
}
if imageInspect.ID != "image_id" {
t.Fatalf("expected `image_id`, got %s", imageInspect.ID)
}
if !reflect.DeepEqual(imageInspect.RepoTags, expectedTags) {
t.Fatalf("expected `%v`, got %v", expectedTags, imageInspect.RepoTags)
}
}

View File

@@ -0,0 +1,40 @@
package client
import (
"encoding/json"
"net/url"
"context"
"github.com/hyperhq/hyper-api/types"
"github.com/hyperhq/hyper-api/types/filters"
)
// ImageList returns a list of images in the docker host.
func (cli *Client) ImageList(ctx context.Context, options types.ImageListOptions) ([]types.Image, error) {
var images []types.Image
query := url.Values{}
if options.Filters.Len() > 0 {
filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filters)
if err != nil {
return images, err
}
query.Set("filters", filterJSON)
}
if options.MatchName != "" {
// FIXME rename this parameter, to not be confused with the filters flag
query.Set("filter", options.MatchName)
}
if options.All {
query.Set("all", "1")
}
serverResp, err := cli.get(ctx, "/images/json", query, nil)
if err != nil {
return images, err
}
err = json.NewDecoder(serverResp.body).Decode(&images)
ensureReaderClosed(serverResp)
return images, err
}

View File

@@ -0,0 +1,122 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"context"
"github.com/hyperhq/hyper-api/types"
"github.com/hyperhq/hyper-api/types/filters"
)
func TestImageListError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ImageList(context.Background(), types.ImageListOptions{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestImageList(t *testing.T) {
expectedURL := "/images/json"
noDanglingfilters := filters.NewArgs()
noDanglingfilters.Add("dangling", "false")
filters := filters.NewArgs()
filters.Add("label", "label1")
filters.Add("label", "label2")
filters.Add("dangling", "true")
listCases := []struct {
options types.ImageListOptions
expectedQueryParams map[string]string
}{
{
options: types.ImageListOptions{},
expectedQueryParams: map[string]string{
"all": "",
"filter": "",
"filters": "",
},
},
{
options: types.ImageListOptions{
All: true,
MatchName: "image_name",
},
expectedQueryParams: map[string]string{
"all": "1",
"filter": "image_name",
"filters": "",
},
},
{
options: types.ImageListOptions{
Filters: filters,
},
expectedQueryParams: map[string]string{
"all": "",
"filter": "",
"filters": `{"dangling":{"true":true},"label":{"label1":true,"label2":true}}`,
},
},
{
options: types.ImageListOptions{
Filters: noDanglingfilters,
},
expectedQueryParams: map[string]string{
"all": "",
"filter": "",
"filters": `{"dangling":{"false":true}}`,
},
},
}
for _, listCase := range listCases {
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
query := req.URL.Query()
for key, expected := range listCase.expectedQueryParams {
actual := query.Get(key)
if actual != expected {
return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
}
}
content, err := json.Marshal([]types.Image{
{
ID: "image_id2",
},
{
ID: "image_id2",
},
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(content)),
}, nil
}),
}
images, err := client.ImageList(context.Background(), listCase.options)
if err != nil {
t.Fatal(err)
}
if len(images) != 2 {
t.Fatalf("expected 2 images, got %v", images)
}
}
}

View File

@@ -0,0 +1,22 @@
package client
import (
"net/url"
"context"
"github.com/hyperhq/hyper-api/types"
)
// ImageLoad loads an image in the docker host from the client host.
// It's up to the caller to close the io.ReadCloser returned by
// this function.
func (cli *Client) ImageLoad(ctx context.Context, input interface{}) (*types.ImageLoadResponse, error) {
resp, err := cli.post(ctx, "/images/load", url.Values{}, input, nil)
if err != nil {
return nil, err
}
return &types.ImageLoadResponse{
Body: resp.body,
JSON: resp.header.Get("Content-Type") == "application/json",
}, nil
}

View File

@@ -0,0 +1,83 @@
package client
import (
"encoding/json"
"errors"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"strconv"
"context"
"github.com/hyperhq/hyper-api/types"
)
func (cli *Client) ImageSaveTarFromDaemon(ctx context.Context, imageIDs []string) (io.ReadCloser, error) {
query := url.Values{
"names": imageIDs,
}
tr := &http.Transport{
Dial: func(proto, addr string) (conn net.Conn, err error) {
return net.Dial("unix", "/var/run/docker.sock")
},
}
client := &http.Client{Transport: tr}
resp, err := client.Get("http://d/images/get?" + query.Encode())
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var dm struct {
Message string `json:"message"`
}
errHead := "Error from local docker daemon: "
if err := json.Unmarshal(data, &dm); err != nil {
return nil, errors.New(errHead + string(data))
}
return nil, errors.New(errHead + dm.Message)
}
return resp.Body, nil
}
func (cli *Client) ImageDiff(ctx context.Context, allLayers [][]string, repoTags [][]string) (*types.ImageDiffResponse, error) {
resp, err := cli.post(ctx, "/images/diff", nil, map[string]interface{}{
"layers": allLayers,
"repoTags": repoTags,
}, nil)
if err != nil {
return nil, err
}
var diffRet types.ImageDiffResponse
err = json.NewDecoder(resp.body).Decode(&diffRet)
ensureReaderClosed(resp)
return &diffRet, nil
}
func (cli *Client) ImageLoadLocal(ctx context.Context, quiet bool, size int64) (*types.HijackedResponse, error) {
query := url.Values{}
query.Add("file", "true")
query.Add("quiet", strconv.FormatBool(quiet))
headers := http.Header{}
headers.Add("X-Hyper-Content-Length", strconv.FormatInt(size, 10))
resp, err := cli.postHijacked(ctx, "/images/load", query, nil, headers)
if err != nil {
return nil, err
}
if resp.Resp != nil && resp.Resp.StatusCode != http.StatusSwitchingProtocols {
data, err := ioutil.ReadAll(resp.Resp.Body)
if err != nil {
return nil, err
}
return nil, errors.New("Error response from daemon: " + string(data))
}
return &resp, nil
}

View File

@@ -0,0 +1,95 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"context"
)
func TestImageLoadError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ImageLoad(context.Background(), nil, true)
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestImageLoad(t *testing.T) {
expectedURL := "/images/load"
expectedInput := "inputBody"
expectedOutput := "outputBody"
loadCases := []struct {
quiet bool
responseContentType string
expectedResponseJSON bool
expectedQueryParams map[string]string
}{
{
quiet: false,
responseContentType: "text/plain",
expectedResponseJSON: false,
expectedQueryParams: map[string]string{
"quiet": "0",
},
},
{
quiet: true,
responseContentType: "application/json",
expectedResponseJSON: true,
expectedQueryParams: map[string]string{
"quiet": "1",
},
},
}
for _, loadCase := range loadCases {
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
contentType := req.Header.Get("Content-Type")
if contentType != "application/x-tar" {
return nil, fmt.Errorf("content-type not set in URL headers properly. Expected 'application/x-tar', got %s", contentType)
}
query := req.URL.Query()
for key, expected := range loadCase.expectedQueryParams {
actual := query.Get(key)
if actual != expected {
return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
}
}
headers := http.Header{}
headers.Add("Content-Type", loadCase.responseContentType)
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(expectedOutput))),
Header: headers,
}, nil
}),
}
input := bytes.NewReader([]byte(expectedInput))
imageLoadResponse, err := client.ImageLoad(context.Background(), input, loadCase.quiet)
if err != nil {
t.Fatal(err)
}
if imageLoadResponse.JSON != loadCase.expectedResponseJSON {
t.Fatalf("expected a JSON response, was not.")
}
body, err := ioutil.ReadAll(imageLoadResponse.Body)
if err != nil {
t.Fatal(err)
}
if string(body) != expectedOutput {
t.Fatalf("expected %s, got %s", expectedOutput, string(body))
}
}
}

View File

@@ -0,0 +1,46 @@
package client
import (
"io"
"net/http"
"net/url"
"context"
"github.com/hyperhq/hyper-api/types"
"github.com/hyperhq/hyper-api/types/reference"
)
// ImagePull requests the docker host to pull an image from a remote registry.
// It executes the privileged function if the operation is unauthorized
// and it tries one more time.
// It's up to the caller to handle the io.ReadCloser and close it properly.
//
// FIXME(vdemeester): there is currently used in a few way in docker/docker
// - if not in trusted content, ref is used to pass the whole reference, and tag is empty
// - if in trusted content, ref is used to pass the reference name, and tag for the digest
func (cli *Client) ImagePull(ctx context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error) {
repository, tag, err := reference.Parse(ref)
if err != nil {
return nil, err
}
query := url.Values{}
query.Set("fromImage", repository)
if tag != "" && !options.All {
query.Set("tag", tag)
}
resp, err := cli.tryImageCreate(ctx, query, options.RegistryAuth)
if resp.statusCode == http.StatusProxyAuthRequired {
newAuthHeader, privilegeErr := options.PrivilegeFunc()
if privilegeErr != nil {
return nil, privilegeErr
}
resp, err = cli.tryImageCreate(ctx, query, newAuthHeader)
}
if err != nil {
return nil, err
}
return resp.body, nil
}

View File

@@ -0,0 +1,199 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"context"
"github.com/hyperhq/hyper-api/types"
)
func TestImagePullReferenceParseError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
return nil, nil
}),
}
// An empty reference is an invalid reference
_, err := client.ImagePull(context.Background(), "", types.ImagePullOptions{})
if err == nil || err.Error() != "repository name must have at least one component" {
t.Fatalf("expected an error, got %v", err)
}
}
func TestImagePullAnyError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ImagePull(context.Background(), "myimage", types.ImagePullOptions{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestImagePullStatusUnauthorizedError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")),
}
_, err := client.ImagePull(context.Background(), "myimage", types.ImagePullOptions{})
if err == nil || err.Error() != "Error response from daemon: Unauthorized error" {
t.Fatalf("expected an Unauthorized Error, got %v", err)
}
}
func TestImagePullWithUnauthorizedErrorAndPrivilegeFuncError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")),
}
privilegeFunc := func() (string, error) {
return "", fmt.Errorf("Error requesting privilege")
}
_, err := client.ImagePull(context.Background(), "myimage", types.ImagePullOptions{
PrivilegeFunc: privilegeFunc,
})
if err == nil || err.Error() != "Error requesting privilege" {
t.Fatalf("expected an error requesting privilege, got %v", err)
}
}
func TestImagePullWithUnauthorizedErrorAndAnotherUnauthorizedError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")),
}
privilegeFunc := func() (string, error) {
return "a-auth-header", nil
}
_, err := client.ImagePull(context.Background(), "myimage", types.ImagePullOptions{
PrivilegeFunc: privilegeFunc,
})
if err == nil || err.Error() != "Error response from daemon: Unauthorized error" {
t.Fatalf("expected an Unauthorized Error, got %v", err)
}
}
func TestImagePullWithPrivilegedFuncNoError(t *testing.T) {
expectedURL := "/images/create"
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
auth := req.Header.Get("X-Registry-Auth")
if auth == "NotValid" {
return &http.Response{
StatusCode: http.StatusUnauthorized,
Body: ioutil.NopCloser(bytes.NewReader([]byte("Invalid credentials"))),
}, nil
}
if auth != "IAmValid" {
return nil, fmt.Errorf("Invalid auth header : expected %s, got %s", "IAmValid", auth)
}
query := req.URL.Query()
fromImage := query.Get("fromImage")
if fromImage != "myimage" {
return nil, fmt.Errorf("fromimage not set in URL query properly. Expected '%s', got %s", "myimage", fromImage)
}
tag := query.Get("tag")
if tag != "latest" {
return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", "latest", tag)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte("hello world"))),
}, nil
}),
}
privilegeFunc := func() (string, error) {
return "IAmValid", nil
}
resp, err := client.ImagePull(context.Background(), "myimage", types.ImagePullOptions{
RegistryAuth: "NotValid",
PrivilegeFunc: privilegeFunc,
})
if err != nil {
t.Fatal(err)
}
body, err := ioutil.ReadAll(resp)
if err != nil {
t.Fatal(err)
}
if string(body) != "hello world" {
t.Fatalf("expected 'hello world', got %s", string(body))
}
}
func TestImagePullWithoutErrors(t *testing.T) {
expectedURL := "/images/create"
expectedOutput := "hello world"
pullCases := []struct {
all bool
reference string
expectedImage string
expectedTag string
}{
{
all: false,
reference: "myimage",
expectedImage: "myimage",
expectedTag: "latest",
},
{
all: false,
reference: "myimage:tag",
expectedImage: "myimage",
expectedTag: "tag",
},
{
all: true,
reference: "myimage",
expectedImage: "myimage",
expectedTag: "",
},
{
all: true,
reference: "myimage:anything",
expectedImage: "myimage",
expectedTag: "",
},
}
for _, pullCase := range pullCases {
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
query := req.URL.Query()
fromImage := query.Get("fromImage")
if fromImage != pullCase.expectedImage {
return nil, fmt.Errorf("fromimage not set in URL query properly. Expected '%s', got %s", pullCase.expectedImage, fromImage)
}
tag := query.Get("tag")
if tag != pullCase.expectedTag {
return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", pullCase.expectedTag, tag)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(expectedOutput))),
}, nil
}),
}
resp, err := client.ImagePull(context.Background(), pullCase.reference, types.ImagePullOptions{
All: pullCase.all,
})
if err != nil {
t.Fatal(err)
}
body, err := ioutil.ReadAll(resp)
if err != nil {
t.Fatal(err)
}
if string(body) != expectedOutput {
t.Fatalf("expected '%s', got %s", expectedOutput, string(body))
}
}
}

View File

@@ -0,0 +1,54 @@
package client
import (
"errors"
"io"
"net/http"
"net/url"
"context"
distreference "github.com/docker/distribution/reference"
"github.com/hyperhq/hyper-api/types"
)
// ImagePush requests the docker host to push an image to a remote registry.
// It executes the privileged function if the operation is unauthorized
// and it tries one more time.
// It's up to the caller to handle the io.ReadCloser and close it properly.
func (cli *Client) ImagePush(ctx context.Context, ref string, options types.ImagePushOptions) (io.ReadCloser, error) {
distributionRef, err := distreference.ParseNamed(ref)
if err != nil {
return nil, err
}
if _, isCanonical := distributionRef.(distreference.Canonical); isCanonical {
return nil, errors.New("cannot push a digest reference")
}
var tag = ""
if nameTaggedRef, isNamedTagged := distributionRef.(distreference.NamedTagged); isNamedTagged {
tag = nameTaggedRef.Tag()
}
query := url.Values{}
query.Set("tag", tag)
resp, err := cli.tryImagePush(ctx, distributionRef.Name(), query, options.RegistryAuth)
if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil {
newAuthHeader, privilegeErr := options.PrivilegeFunc()
if privilegeErr != nil {
return nil, privilegeErr
}
resp, err = cli.tryImagePush(ctx, distributionRef.Name(), query, newAuthHeader)
}
if err != nil {
return nil, err
}
return resp.body, nil
}
func (cli *Client) tryImagePush(ctx context.Context, imageID string, query url.Values, registryAuth string) (*serverResponse, error) {
headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
return cli.post(ctx, "/images/"+imageID+"/push", query, nil, headers)
}

View File

@@ -0,0 +1,180 @@
package client
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"context"
"github.com/hyperhq/hyper-api/types"
)
func TestImagePushReferenceError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
return nil, nil
}),
}
// An empty reference is an invalid reference
_, err := client.ImagePush(context.Background(), "", types.ImagePushOptions{})
if err == nil || err.Error() != "repository name must have at least one component" {
t.Fatalf("expected an error, got %v", err)
}
// An canonical reference cannot be pushed
_, err = client.ImagePush(context.Background(), "repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", types.ImagePushOptions{})
if err == nil || err.Error() != "cannot push a digest reference" {
t.Fatalf("expected an error, got %v", err)
}
}
func TestImagePushAnyError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ImagePush(context.Background(), "myimage", types.ImagePushOptions{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestImagePushStatusUnauthorizedError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")),
}
_, err := client.ImagePush(context.Background(), "myimage", types.ImagePushOptions{})
if err == nil || err.Error() != "Error response from daemon: Unauthorized error" {
t.Fatalf("expected an Unauthorized Error, got %v", err)
}
}
func TestImagePushWithUnauthorizedErrorAndPrivilegeFuncError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")),
}
privilegeFunc := func() (string, error) {
return "", fmt.Errorf("Error requesting privilege")
}
_, err := client.ImagePush(context.Background(), "myimage", types.ImagePushOptions{
PrivilegeFunc: privilegeFunc,
})
if err == nil || err.Error() != "Error requesting privilege" {
t.Fatalf("expected an error requesting privilege, got %v", err)
}
}
func TestImagePushWithUnauthorizedErrorAndAnotherUnauthorizedError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")),
}
privilegeFunc := func() (string, error) {
return "a-auth-header", nil
}
_, err := client.ImagePush(context.Background(), "myimage", types.ImagePushOptions{
PrivilegeFunc: privilegeFunc,
})
if err == nil || err.Error() != "Error response from daemon: Unauthorized error" {
t.Fatalf("expected an Unauthorized Error, got %v", err)
}
}
func TestImagePushWithPrivilegedFuncNoError(t *testing.T) {
expectedURL := "/images/myimage/push"
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
auth := req.Header.Get("X-Registry-Auth")
if auth == "NotValid" {
return &http.Response{
StatusCode: http.StatusUnauthorized,
Body: ioutil.NopCloser(bytes.NewReader([]byte("Invalid credentials"))),
}, nil
}
if auth != "IAmValid" {
return nil, fmt.Errorf("Invalid auth header : expected %s, got %s", "IAmValid", auth)
}
query := req.URL.Query()
tag := query.Get("tag")
if tag != "tag" {
return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", "tag", tag)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte("hello world"))),
}, nil
}),
}
privilegeFunc := func() (string, error) {
return "IAmValid", nil
}
resp, err := client.ImagePush(context.Background(), "myimage:tag", types.ImagePushOptions{
RegistryAuth: "NotValid",
PrivilegeFunc: privilegeFunc,
})
if err != nil {
t.Fatal(err)
}
body, err := ioutil.ReadAll(resp)
if err != nil {
t.Fatal(err)
}
if string(body) != "hello world" {
t.Fatalf("expected 'hello world', got %s", string(body))
}
}
func TestImagePushWithoutErrors(t *testing.T) {
expectedOutput := "hello world"
expectedURLFormat := "/images/%s/push"
pullCases := []struct {
reference string
expectedImage string
expectedTag string
}{
{
reference: "myimage",
expectedImage: "myimage",
expectedTag: "",
},
{
reference: "myimage:tag",
expectedImage: "myimage",
expectedTag: "tag",
},
}
for _, pullCase := range pullCases {
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
expectedURL := fmt.Sprintf(expectedURLFormat, pullCase.expectedImage)
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
query := req.URL.Query()
tag := query.Get("tag")
if tag != pullCase.expectedTag {
return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", pullCase.expectedTag, tag)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(expectedOutput))),
}, nil
}),
}
resp, err := client.ImagePush(context.Background(), pullCase.reference, types.ImagePushOptions{})
if err != nil {
t.Fatal(err)
}
body, err := ioutil.ReadAll(resp)
if err != nil {
t.Fatal(err)
}
if string(body) != expectedOutput {
t.Fatalf("expected '%s', got %s", expectedOutput, string(body))
}
}
}

View File

@@ -0,0 +1,31 @@
package client
import (
"context"
"encoding/json"
"net/url"
"github.com/hyperhq/hyper-api/types"
)
// ImageRemove removes an image from the docker host.
func (cli *Client) ImageRemove(ctx context.Context, imageID string, options types.ImageRemoveOptions) ([]types.ImageDelete, error) {
query := url.Values{}
if options.Force {
query.Set("force", "1")
}
if !options.PruneChildren {
query.Set("noprune", "1")
}
resp, err := cli.delete(ctx, "/images/"+imageID, query, nil)
if err != nil {
return nil, err
}
var dels []types.ImageDelete
err = json.NewDecoder(resp.body).Decode(&dels)
ensureReaderClosed(resp)
return dels, err
}

View File

@@ -0,0 +1,95 @@
package client
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"github.com/hyperhq/hyper-api/types"
)
func TestImageRemoveError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ImageRemove(context.Background(), "image_id", types.ImageRemoveOptions{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestImageRemove(t *testing.T) {
expectedURL := "/images/image_id"
removeCases := []struct {
force bool
pruneChildren bool
expectedQueryParams map[string]string
}{
{
force: false,
pruneChildren: false,
expectedQueryParams: map[string]string{
"force": "",
"noprune": "1",
},
}, {
force: true,
pruneChildren: true,
expectedQueryParams: map[string]string{
"force": "1",
"noprune": "",
},
},
}
for _, removeCase := range removeCases {
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("expected URL '%s', got '%s'", expectedURL, req.URL)
}
if req.Method != "DELETE" {
return nil, fmt.Errorf("expected DELETE method, got %s", req.Method)
}
query := req.URL.Query()
for key, expected := range removeCase.expectedQueryParams {
actual := query.Get(key)
if actual != expected {
return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
}
}
b, err := json.Marshal([]types.ImageDelete{
{
Untagged: "image_id1",
},
{
Deleted: "image_id",
},
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(b)),
}, nil
}),
}
imageDeletes, err := client.ImageRemove(context.Background(), "image_id", types.ImageRemoveOptions{
Force: removeCase.force,
PruneChildren: removeCase.pruneChildren,
})
if err != nil {
t.Fatal(err)
}
if len(imageDeletes) != 2 {
t.Fatalf("expected 2 deleted images, got %v", imageDeletes)
}
}
}

View File

@@ -0,0 +1,21 @@
package client
import (
"context"
"io"
"net/url"
)
// ImageSave retrieves one or more images from the docker host as an io.ReadCloser.
// It's up to the caller to store the images and close the stream.
func (cli *Client) ImageSave(ctx context.Context, imageIDs []string) (io.ReadCloser, error) {
query := url.Values{
"names": imageIDs,
}
resp, err := cli.get(ctx, "/images/get", query, nil)
if err != nil {
return nil, err
}
return resp.body, nil
}

View File

@@ -0,0 +1,56 @@
package client
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"net/http"
"reflect"
"strings"
"testing"
)
func TestImageSaveError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ImageSave(context.Background(), []string{"nothing"})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server error, got %v", err)
}
}
func TestImageSave(t *testing.T) {
expectedURL := "/images/get"
client := &Client{
transport: newMockClient(nil, func(r *http.Request) (*http.Response, error) {
if !strings.HasPrefix(r.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, r.URL)
}
query := r.URL.Query()
names := query["names"]
expectedNames := []string{"image_id1", "image_id2"}
if !reflect.DeepEqual(names, expectedNames) {
return nil, fmt.Errorf("names not set in URL query properly. Expected %v, got %v", names, expectedNames)
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte("response"))),
}, nil
}),
}
saveResponse, err := client.ImageSave(context.Background(), []string{"image_id1", "image_id2"})
if err != nil {
t.Fatal(err)
}
response, err := ioutil.ReadAll(saveResponse)
if err != nil {
t.Fatal(err)
}
saveResponse.Close()
if string(response) != "response" {
t.Fatalf("expected response to contain 'response', got %s", string(response))
}
}

View File

@@ -0,0 +1,49 @@
package client
import (
"context"
"encoding/json"
"net/http"
"net/url"
"github.com/hyperhq/hyper-api/types"
"github.com/hyperhq/hyper-api/types/filters"
"github.com/hyperhq/hyper-api/types/registry"
)
// ImageSearch makes the docker host to search by a term in a remote registry.
// The list of results is not sorted in any fashion.
func (cli *Client) ImageSearch(ctx context.Context, term string, options types.ImageSearchOptions) ([]registry.SearchResult, error) {
var results []registry.SearchResult
query := url.Values{}
query.Set("term", term)
if options.Filters.Len() > 0 {
filterJSON, err := filters.ToParam(options.Filters)
if err != nil {
return results, err
}
query.Set("filters", filterJSON)
}
resp, err := cli.tryImageSearch(ctx, query, options.RegistryAuth)
if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil {
newAuthHeader, privilegeErr := options.PrivilegeFunc()
if privilegeErr != nil {
return results, privilegeErr
}
resp, err = cli.tryImageSearch(ctx, query, newAuthHeader)
}
if err != nil {
return results, err
}
err = json.NewDecoder(resp.body).Decode(&results)
ensureReaderClosed(resp)
return results, err
}
func (cli *Client) tryImageSearch(ctx context.Context, query url.Values, registryAuth string) (*serverResponse, error) {
headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
return cli.get(ctx, "/images/search", query, headers)
}

View File

@@ -0,0 +1,164 @@
package client
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"github.com/hyperhq/hyper-api/types"
"github.com/hyperhq/hyper-api/types/filters"
"github.com/hyperhq/hyper-api/types/registry"
)
func TestImageSearchAnyError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
func TestImageSearchStatusUnauthorizedError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")),
}
_, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{})
if err == nil || err.Error() != "Error response from daemon: Unauthorized error" {
t.Fatalf("expected an Unauthorized Error, got %v", err)
}
}
func TestImageSearchWithUnauthorizedErrorAndPrivilegeFuncError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")),
}
privilegeFunc := func() (string, error) {
return "", fmt.Errorf("Error requesting privilege")
}
_, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{
PrivilegeFunc: privilegeFunc,
})
if err == nil || err.Error() != "Error requesting privilege" {
t.Fatalf("expected an error requesting privilege, got %v", err)
}
}
func TestImageSearchWithUnauthorizedErrorAndAnotherUnauthorizedError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusUnauthorized, "Unauthorized error")),
}
privilegeFunc := func() (string, error) {
return "a-auth-header", nil
}
_, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{
PrivilegeFunc: privilegeFunc,
})
if err == nil || err.Error() != "Error response from daemon: Unauthorized error" {
t.Fatalf("expected an Unauthorized Error, got %v", err)
}
}
func TestImageSearchWithPrivilegedFuncNoError(t *testing.T) {
expectedURL := "/images/search"
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
auth := req.Header.Get("X-Registry-Auth")
if auth == "NotValid" {
return &http.Response{
StatusCode: http.StatusUnauthorized,
Body: ioutil.NopCloser(bytes.NewReader([]byte("Invalid credentials"))),
}, nil
}
if auth != "IAmValid" {
return nil, fmt.Errorf("Invalid auth header : expected %s, got %s", "IAmValid", auth)
}
query := req.URL.Query()
term := query.Get("term")
if term != "some-image" {
return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", "some-image", term)
}
content, err := json.Marshal([]registry.SearchResult{
{
Name: "anything",
},
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(content)),
}, nil
}),
}
privilegeFunc := func() (string, error) {
return "IAmValid", nil
}
results, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{
RegistryAuth: "NotValid",
PrivilegeFunc: privilegeFunc,
})
if err != nil {
t.Fatal(err)
}
if len(results) != 1 {
t.Fatalf("expected a result, got %v", results)
}
}
func TestImageSearchWithoutErrors(t *testing.T) {
expectedURL := "/images/search"
filterArgs := filters.NewArgs()
filterArgs.Add("is-automated", "true")
filterArgs.Add("stars", "3")
expectedFilters := `{"is-automated":{"true":true},"stars":{"3":true}}`
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
}
query := req.URL.Query()
term := query.Get("term")
if term != "some-image" {
return nil, fmt.Errorf("tag not set in URL query properly. Expected '%s', got %s", "some-image", term)
}
filters := query.Get("filters")
if filters != expectedFilters {
return nil, fmt.Errorf("filters not set in URL query properly. Expected '%s', got %s", expectedFilters, filters)
}
content, err := json.Marshal([]registry.SearchResult{
{
Name: "anything",
},
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(content)),
}, nil
}),
}
results, err := client.ImageSearch(context.Background(), "some-image", types.ImageSearchOptions{
Filters: filterArgs,
})
if err != nil {
t.Fatal(err)
}
if len(results) != 1 {
t.Fatalf("expected a result, got %v", results)
}
}

View File

@@ -0,0 +1,37 @@
package client
import (
"context"
"errors"
"fmt"
"net/url"
distreference "github.com/docker/distribution/reference"
"github.com/hyperhq/hyper-api/types"
"github.com/hyperhq/hyper-api/types/reference"
)
// ImageTag tags an image in the docker host
func (cli *Client) ImageTag(ctx context.Context, imageID, ref string, options types.ImageTagOptions) error {
distributionRef, err := distreference.ParseNamed(ref)
if err != nil {
return fmt.Errorf("Error parsing reference: %q is not a valid repository/tag", ref)
}
if _, isCanonical := distributionRef.(distreference.Canonical); isCanonical {
return errors.New("refusing to create a tag with a digest reference")
}
tag := reference.GetTagFromNamedRef(distributionRef)
query := url.Values{}
query.Set("repo", distributionRef.Name())
query.Set("tag", tag)
if options.Force {
query.Set("force", "1")
}
resp, err := cli.post(ctx, "/images/"+imageID+"/tag", query, nil, nil)
ensureReaderClosed(resp)
return err
}

View File

@@ -0,0 +1,120 @@
package client
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
)
func TestImageTagError(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
err := client.ImageTag(context.Background(), "image_id", "repo:tag")
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
}
// Note: this is not testing all the InvalidReference as it's the reponsability
// of distribution/reference package.
func TestImageTagInvalidReference(t *testing.T) {
client := &Client{
transport: newMockClient(nil, errorMock(http.StatusInternalServerError, "Server error")),
}
err := client.ImageTag(context.Background(), "image_id", "aa/asdf$$^/aa")
if err == nil || err.Error() != `Error parsing reference: "aa/asdf$$^/aa" is not a valid repository/tag` {
t.Fatalf("expected ErrReferenceInvalidFormat, got %v", err)
}
}
func TestImageTag(t *testing.T) {
expectedURL := "/images/image_id/tag"
tagCases := []struct {
reference string
expectedQueryParams map[string]string
}{
{
reference: "repository:tag1",
expectedQueryParams: map[string]string{
"repo": "repository",
"tag": "tag1",
},
}, {
reference: "another_repository:latest",
expectedQueryParams: map[string]string{
"repo": "another_repository",
"tag": "latest",
},
}, {
reference: "another_repository",
expectedQueryParams: map[string]string{
"repo": "another_repository",
"tag": "latest",
},
}, {
reference: "test/another_repository",
expectedQueryParams: map[string]string{
"repo": "test/another_repository",
"tag": "latest",
},
}, {
reference: "test/another_repository:tag1",
expectedQueryParams: map[string]string{
"repo": "test/another_repository",
"tag": "tag1",
},
}, {
reference: "test/test/another_repository:tag1",
expectedQueryParams: map[string]string{
"repo": "test/test/another_repository",
"tag": "tag1",
},
}, {
reference: "test:5000/test/another_repository:tag1",
expectedQueryParams: map[string]string{
"repo": "test:5000/test/another_repository",
"tag": "tag1",
},
}, {
reference: "test:5000/test/another_repository",
expectedQueryParams: map[string]string{
"repo": "test:5000/test/another_repository",
"tag": "latest",
},
},
}
for _, tagCase := range tagCases {
client := &Client{
transport: newMockClient(nil, func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("expected URL '%s', got '%s'", expectedURL, req.URL)
}
if req.Method != "POST" {
return nil, fmt.Errorf("expected POST method, got %s", req.Method)
}
query := req.URL.Query()
for key, expected := range tagCase.expectedQueryParams {
actual := query.Get(key)
if actual != expected {
return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
}
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
}, nil
}),
}
err := client.ImageTag(context.Background(), "image_id", tagCase.reference)
if err != nil {
t.Fatal(err)
}
}
}

Some files were not shown because too many files have changed in this diff Show More