First commit

This commit is contained in:
Joe Beda
2014-06-06 16:40:48 -07:00
commit 2c4b3a562c
250 changed files with 47501 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
language: go
go:
- 1.1.2
- 1.2
- tip
env:
- GOARCH=amd64
- GOARCH=386
install:
- go get -d ./...
script:
- go test ./...

View File

@@ -0,0 +1,29 @@
# This is the official list of go-dockerclient authors for copyright purposes.
Andrews Medina <andrewsmedina@gmail.com>
Andy Goldstein <andy.goldstein@redhat.com>
Ben McCann <benmccann.com>
Cezar Sa Espinola <cezar.sa@corp.globo.com>
cheneydeng <cheneydeng@qq.com>
Ed <edrocksit@gmail.com>
Eric Anderson <anderson@copperegg.com>
Flavia Missi <flaviamissi@gmail.com>
Francisco Souza <f@souza.cc>
Jason Wilder <jwilder@litl.com>
Jean-Baptiste Dalido <jeanbaptiste@appgratis.com>
Jeff Mitchell <jeffrey.mitchell@gmail.com>
Jeffrey Hulten <jhulten@gmail.com>
Lucas Clemente <lucas@clemente.io>
Paul Morie <pmorie@gmail.com>
Peter Jihoon Kim <raingrove@gmail.com>
Philippe Lafoucrière <philippe.lafoucriere@tech-angels.com>
Salvador Gironès <salvadorgirones@gmail.com>
Simon Eskildsen <sirup@sirupsen.com>
Simon Menke <simon.menke@gmail.com>
Skolos <skolos@gopherlab.com>
Soulou <leo@unbekandt.eu>
Sridhar Ratnakumar <sridharr@activestate.com>
Summer Mousa <smousa@zenoss.com>
Tarsis Azevedo <tarsis@corp.globo.com>
Tim Schindler <tim@catalyst-zero.com>
Wiliam Souza <wiliamsouza83@gmail.com>

View File

@@ -0,0 +1,6 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
You can find the Docker license int the following link:
https://raw2.github.com/dotcloud/docker/master/LICENSE

View File

@@ -0,0 +1,22 @@
Copyright (c) 2014, go-dockerclient authors
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,47 @@
#go-dockerclient
[![Build Status](https://drone.io/github.com/fsouza/go-dockerclient/status.png)](https://drone.io/github.com/fsouza/go-dockerclient/latest)
[![Build Status](https://travis-ci.org/fsouza/go-dockerclient.png)](https://travis-ci.org/fsouza/go-dockerclient)
[![GoDoc](http://godoc.org/github.com/fsouza/go-dockerclient?status.png)](http://godoc.org/github.com/fsouza/go-dockerclient)
This package presents a client for the Docker remote API.
For more details, check the [remote API documentation](http://docs.docker.io/en/latest/reference/api/docker_remote_api/).
##Versioning
* Version 0.1 is compatible with Docker v0.7.1
* The master is compatible with Docker's master
## Example
package main
import (
"fmt"
"github.com/fsouza/go-dockerclient"
)
func main() {
endpoint := "unix:///var/run/docker.sock"
client, _ := docker.NewClient(endpoint)
imgs, _ := client.ListImages(true)
for _, img := range imgs {
fmt.Println("ID: ", img.ID)
fmt.Println("RepoTags: ", img.RepoTags)
fmt.Println("Created: ", img.Created)
fmt.Println("Size: ", img.Size)
fmt.Println("VirtualSize: ", img.VirtualSize)
fmt.Println("ParentId: ", img.ParentId)
fmt.Println("Repository: ", img.Repository)
}
}
## Developing
You can run the tests with:
go get -d ./...
go test ./...

View File

@@ -0,0 +1,36 @@
// Copyright 2014 go-dockerclient authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package docker
import "fmt"
type ChangeType int
const (
ChangeModify ChangeType = iota
ChangeAdd
ChangeDelete
)
// Change represents a change in a container.
//
// See http://goo.gl/DpGyzK for more details.
type Change struct {
Path string
Kind ChangeType
}
func (change *Change) String() string {
var kind string
switch change.Kind {
case ChangeModify:
kind = "C"
case ChangeAdd:
kind = "A"
case ChangeDelete:
kind = "D"
}
return fmt.Sprintf("%s %s", kind, change.Path)
}

View File

@@ -0,0 +1,26 @@
// Copyright 2014 go-dockerclient authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package docker
import (
"testing"
)
func TestChangeString(t *testing.T) {
var tests = []struct {
change Change
expected string
}{
{Change{"/etc/passwd", ChangeModify}, "C /etc/passwd"},
{Change{"/etc/passwd", ChangeAdd}, "A /etc/passwd"},
{Change{"/etc/passwd", ChangeDelete}, "D /etc/passwd"},
{Change{"/etc/passwd", 33}, " /etc/passwd"},
}
for _, tt := range tests {
if got := tt.change.String(); got != tt.expected {
t.Errorf("Change.String(): want %q. Got %q.", tt.expected, got)
}
}
}

View File

@@ -0,0 +1,352 @@
// Copyright 2014 go-dockerclient authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package docker provides a client for the Docker remote API.
//
// See http://goo.gl/mxyql for more details on the remote API.
package docker
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/fsouza/go-dockerclient/utils"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httputil"
"net/url"
"reflect"
"strconv"
"strings"
"sync"
)
const userAgent = "go-dockerclient"
var (
// ErrInvalidEndpoint is returned when the endpoint is not a valid HTTP URL.
ErrInvalidEndpoint = errors.New("invalid endpoint")
// ErrConnectionRefused is returned when the client cannot connect to the given endpoint.
ErrConnectionRefused = errors.New("cannot connect to Docker endpoint")
)
// Client is the basic type of this package. It provides methods for
// interaction with the API.
type Client struct {
endpoint string
endpointURL *url.URL
eventMonitor *eventMonitoringState
client *http.Client
}
// NewClient returns a Client instance ready for communication with the
// given server endpoint.
func NewClient(endpoint string) (*Client, error) {
u, err := parseEndpoint(endpoint)
if err != nil {
return nil, err
}
return &Client{
endpoint: endpoint,
endpointURL: u,
client: http.DefaultClient,
eventMonitor: new(eventMonitoringState),
}, nil
}
func (c *Client) do(method, path string, data interface{}) ([]byte, int, error) {
var params io.Reader
if data != nil {
buf, err := json.Marshal(data)
if err != nil {
return nil, -1, err
}
params = bytes.NewBuffer(buf)
}
req, err := http.NewRequest(method, c.getURL(path), params)
if err != nil {
return nil, -1, err
}
req.Header.Set("User-Agent", userAgent)
if data != nil {
req.Header.Set("Content-Type", "application/json")
} else if method == "POST" {
req.Header.Set("Content-Type", "plain/text")
}
var resp *http.Response
protocol := c.endpointURL.Scheme
address := c.endpointURL.Path
if protocol == "unix" {
dial, err := net.Dial(protocol, address)
if err != nil {
return nil, -1, err
}
clientconn := httputil.NewClientConn(dial, nil)
resp, err = clientconn.Do(req)
defer clientconn.Close()
} else {
resp, err = c.client.Do(req)
}
if err != nil {
if strings.Contains(err.Error(), "connection refused") {
return nil, -1, ErrConnectionRefused
}
return nil, -1, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, -1, err
}
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
return nil, resp.StatusCode, newError(resp.StatusCode, body)
}
return body, resp.StatusCode, nil
}
func (c *Client) stream(method, path string, headers map[string]string, in io.Reader, out io.Writer) error {
if (method == "POST" || method == "PUT") && in == nil {
in = bytes.NewReader(nil)
}
req, err := http.NewRequest(method, c.getURL(path), in)
if err != nil {
return err
}
req.Header.Set("User-Agent", userAgent)
if method == "POST" {
req.Header.Set("Content-Type", "plain/text")
}
for key, val := range headers {
req.Header.Set(key, val)
}
var resp *http.Response
protocol := c.endpointURL.Scheme
address := c.endpointURL.Path
if out == nil {
out = ioutil.Discard
}
if protocol == "unix" {
dial, err := net.Dial(protocol, address)
if err != nil {
return err
}
clientconn := httputil.NewClientConn(dial, nil)
resp, err = clientconn.Do(req)
defer clientconn.Close()
} else {
resp, err = c.client.Do(req)
}
if err != nil {
if strings.Contains(err.Error(), "connection refused") {
return ErrConnectionRefused
}
return err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return newError(resp.StatusCode, body)
}
if resp.Header.Get("Content-Type") == "application/json" {
dec := json.NewDecoder(resp.Body)
for {
var m jsonMessage
if err := dec.Decode(&m); err == io.EOF {
break
} else if err != nil {
return err
}
if m.Stream != "" {
fmt.Fprint(out, m.Stream)
} else if m.Progress != "" {
fmt.Fprintf(out, "%s %s\r", m.Status, m.Progress)
} else if m.Error != "" {
return errors.New(m.Error)
}
if m.Status != "" {
fmt.Fprintln(out, m.Status)
}
}
} else {
if _, err := io.Copy(out, resp.Body); err != nil {
return err
}
}
return nil
}
func (c *Client) hijack(method, path string, success chan struct{}, in io.Reader, errStream io.Writer, out io.Writer) error {
req, err := http.NewRequest(method, c.getURL(path), nil)
if err != nil {
return err
}
req.Header.Set("Content-Type", "plain/text")
protocol := c.endpointURL.Scheme
address := c.endpointURL.Path
if protocol != "unix" {
protocol = "tcp"
address = c.endpointURL.Host
}
dial, err := net.Dial(protocol, address)
if err != nil {
return err
}
defer dial.Close()
clientconn := httputil.NewClientConn(dial, nil)
clientconn.Do(req)
if success != nil {
success <- struct{}{}
<-success
}
rwc, br := clientconn.Hijack()
var wg sync.WaitGroup
wg.Add(2)
errs := make(chan error, 2)
go func() {
var err error
if in != nil {
_, err = io.Copy(out, br)
} else {
_, err = utils.StdCopy(out, errStream, br)
}
errs <- err
wg.Done()
}()
go func() {
var err error
if in != nil {
_, err = io.Copy(rwc, in)
}
rwc.(interface {
CloseWrite() error
}).CloseWrite()
errs <- err
wg.Done()
}()
wg.Wait()
close(errs)
if err := <-errs; err != nil {
return err
}
return nil
}
func (c *Client) getURL(path string) string {
urlStr := strings.TrimRight(c.endpointURL.String(), "/")
if c.endpointURL.Scheme == "unix" {
urlStr = ""
}
return fmt.Sprintf("%s%s", urlStr, path)
}
type jsonMessage struct {
Status string `json:"status,omitempty"`
Progress string `json:"progress,omitempty"`
Error string `json:"error,omitempty"`
Stream string `json:"stream,omitempty"`
}
func queryString(opts interface{}) string {
if opts == nil {
return ""
}
value := reflect.ValueOf(opts)
if value.Kind() == reflect.Ptr {
value = value.Elem()
}
if value.Kind() != reflect.Struct {
return ""
}
items := url.Values(map[string][]string{})
for i := 0; i < value.NumField(); i++ {
field := value.Type().Field(i)
if field.PkgPath != "" {
continue
}
key := field.Tag.Get("qs")
if key == "" {
key = strings.ToLower(field.Name)
} else if key == "-" {
continue
}
v := value.Field(i)
switch v.Kind() {
case reflect.Bool:
if v.Bool() {
items.Add(key, "1")
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if v.Int() > 0 {
items.Add(key, strconv.FormatInt(v.Int(), 10))
}
case reflect.Float32, reflect.Float64:
if v.Float() > 0 {
items.Add(key, strconv.FormatFloat(v.Float(), 'f', -1, 64))
}
case reflect.String:
if v.String() != "" {
items.Add(key, v.String())
}
case reflect.Ptr:
if !v.IsNil() {
if b, err := json.Marshal(v.Interface()); err == nil {
items.Add(key, string(b))
}
}
}
}
return items.Encode()
}
// Error represents failures in the API. It represents a failure from the API.
type Error struct {
Status int
Message string
}
func newError(status int, body []byte) *Error {
return &Error{Status: status, Message: string(body)}
}
func (e *Error) Error() string {
return fmt.Sprintf("API error (%d): %s", e.Status, e.Message)
}
func parseEndpoint(endpoint string) (*url.URL, error) {
u, err := url.Parse(endpoint)
if err != nil {
return nil, ErrInvalidEndpoint
}
if u.Scheme == "tcp" {
u.Scheme = "http"
}
if u.Scheme != "http" && u.Scheme != "https" && u.Scheme != "unix" {
return nil, ErrInvalidEndpoint
}
if u.Scheme != "unix" {
_, port, err := net.SplitHostPort(u.Host)
if err != nil {
if e, ok := err.(*net.AddrError); ok {
if e.Err == "missing port in address" {
return u, nil
}
}
return nil, ErrInvalidEndpoint
}
number, err := strconv.ParseInt(port, 10, 64)
if err == nil && number > 0 && number < 65536 {
return u, nil
}
} else {
return u, nil // we don't need port when using a unix socket
}
return nil, ErrInvalidEndpoint
}

View File

@@ -0,0 +1,161 @@
// Copyright 2014 go-dockerclient authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package docker
import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"reflect"
"strconv"
"strings"
"testing"
)
func TestNewAPIClient(t *testing.T) {
endpoint := "http://localhost:4243"
client, err := NewClient(endpoint)
if err != nil {
t.Fatal(err)
}
if client.endpoint != endpoint {
t.Errorf("Expected endpoint %s. Got %s.", endpoint, client.endpoint)
}
if client.client != http.DefaultClient {
t.Errorf("Expected http.Client %#v. Got %#v.", http.DefaultClient, client.client)
}
// test unix socket endpoints
endpoint = "unix:///var/run/docker.sock"
client, err = NewClient(endpoint)
if err != nil {
t.Fatal(err)
}
if client.endpoint != endpoint {
t.Errorf("Expected endpoint %s. Got %s.", endpoint, client.endpoint)
}
}
func TestNewClientInvalidEndpoint(t *testing.T) {
cases := []string{
"htp://localhost:3243", "http://localhost:a", "localhost:8080",
"", "localhost", "http://localhost:8080:8383", "http://localhost:65536",
"https://localhost:-20",
}
for _, c := range cases {
client, err := NewClient(c)
if client != nil {
t.Errorf("Want <nil> client for invalid endpoint, got %#v.", client)
}
if !reflect.DeepEqual(err, ErrInvalidEndpoint) {
t.Errorf("NewClient(%q): Got invalid error for invalid endpoint. Want %#v. Got %#v.", c, ErrInvalidEndpoint, err)
}
}
}
func TestGetURL(t *testing.T) {
var tests = []struct {
endpoint string
path string
expected string
}{
{"http://localhost:4243/", "/", "http://localhost:4243/"},
{"http://localhost:4243", "/", "http://localhost:4243/"},
{"http://localhost:4243", "/containers/ps", "http://localhost:4243/containers/ps"},
{"tcp://localhost:4243", "/containers/ps", "http://localhost:4243/containers/ps"},
{"http://localhost:4243/////", "/", "http://localhost:4243/"},
{"unix:///var/run/docker.socket", "/containers", "/containers"},
}
for _, tt := range tests {
client, _ := NewClient(tt.endpoint)
client.endpoint = tt.endpoint
got := client.getURL(tt.path)
if got != tt.expected {
t.Errorf("getURL(%q): Got %s. Want %s.", tt.path, got, tt.expected)
}
}
}
func TestError(t *testing.T) {
err := newError(400, []byte("bad parameter"))
expected := Error{Status: 400, Message: "bad parameter"}
if !reflect.DeepEqual(expected, *err) {
t.Errorf("Wrong error type. Want %#v. Got %#v.", expected, *err)
}
message := "API error (400): bad parameter"
if err.Error() != message {
t.Errorf("Wrong error message. Want %q. Got %q.", message, err.Error())
}
}
func TestQueryString(t *testing.T) {
v := float32(2.4)
f32QueryString := fmt.Sprintf("w=%s&x=10&y=10.35", strconv.FormatFloat(float64(v), 'f', -1, 64))
jsonPerson := url.QueryEscape(`{"Name":"gopher","age":4}`)
var tests = []struct {
input interface{}
want string
}{
{&ListContainersOptions{All: true}, "all=1"},
{ListContainersOptions{All: true}, "all=1"},
{ListContainersOptions{Before: "something"}, "before=something"},
{ListContainersOptions{Before: "something", Since: "other"}, "before=something&since=other"},
{dumb{X: 10, Y: 10.35000}, "x=10&y=10.35"},
{dumb{W: v, X: 10, Y: 10.35000}, f32QueryString},
{dumb{X: 10, Y: 10.35000, Z: 10}, "x=10&y=10.35&zee=10"},
{dumb{v: 4, X: 10, Y: 10.35000}, "x=10&y=10.35"},
{dumb{T: 10, Y: 10.35000}, "y=10.35"},
{dumb{Person: &person{Name: "gopher", Age: 4}}, "p=" + jsonPerson},
{nil, ""},
{10, ""},
{"not_a_struct", ""},
}
for _, tt := range tests {
got := queryString(tt.input)
if got != tt.want {
t.Errorf("queryString(%v). Want %q. Got %q.", tt.input, tt.want, got)
}
}
}
type FakeRoundTripper struct {
message string
status int
requests []*http.Request
}
func (rt *FakeRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
body := strings.NewReader(rt.message)
rt.requests = append(rt.requests, r)
return &http.Response{
StatusCode: rt.status,
Body: ioutil.NopCloser(body),
}, nil
}
func (rt *FakeRoundTripper) Reset() {
rt.requests = nil
}
type person struct {
Name string
Age int `json:"age"`
}
type dumb struct {
T int `qs:"-"`
v int
W float32
X int
Y float64
Z int `qs:"zee"`
Person *person `qs:"p"`
}
type fakeEndpointURL struct {
Scheme string
}

View File

@@ -0,0 +1,583 @@
// Copyright 2014 go-dockerclient authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package docker
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
)
// ListContainersOptions specify parameters to the ListContainers function.
//
// See http://goo.gl/QpCnDN for more details.
type ListContainersOptions struct {
All bool
Size bool
Limit int
Since string
Before string
}
type APIPort struct {
PrivatePort int64
PublicPort int64
Type string
IP string
}
// APIContainers represents a container.
//
// See http://goo.gl/QeFH7U for more details.
type APIContainers struct {
ID string `json:"Id"`
Image string
Command string
Created int64
Status string
Ports []APIPort
SizeRw int64
SizeRootFs int64
Names []string
}
// ListContainers returns a slice of containers matching the given criteria.
//
// See http://goo.gl/QpCnDN for more details.
func (c *Client) ListContainers(opts ListContainersOptions) ([]APIContainers, error) {
path := "/containers/json?" + queryString(opts)
body, _, err := c.do("GET", path, nil)
if err != nil {
return nil, err
}
var containers []APIContainers
err = json.Unmarshal(body, &containers)
if err != nil {
return nil, err
}
return containers, nil
}
// Port represents the port number and the protocol, in the form
// <number>/<protocol>. For example: 80/tcp.
type Port string
// Port returns the number of the port.
func (p Port) Port() string {
return strings.Split(string(p), "/")[0]
}
// Proto returns the name of the protocol.
func (p Port) Proto() string {
parts := strings.Split(string(p), "/")
if len(parts) == 1 {
return "tcp"
}
return parts[1]
}
// State represents the state of a container.
type State struct {
sync.RWMutex
Running bool
Pid int
ExitCode int
StartedAt time.Time
FinishedAt time.Time
Ghost bool
}
// String returns the string representation of a state.
func (s *State) String() string {
s.RLock()
defer s.RUnlock()
if s.Running {
if s.Ghost {
return "Ghost"
}
return fmt.Sprintf("Up %s", time.Now().UTC().Sub(s.StartedAt))
}
return fmt.Sprintf("Exit %d", s.ExitCode)
}
type PortBinding struct {
HostIp string
HostPort string
}
type PortMapping map[string]string
type NetworkSettings struct {
IPAddress string
IPPrefixLen int
Gateway string
Bridge string
PortMapping map[string]PortMapping
Ports map[Port][]PortBinding
}
func (settings *NetworkSettings) PortMappingAPI() []APIPort {
var mapping []APIPort
for port, bindings := range settings.Ports {
p, _ := parsePort(port.Port())
if len(bindings) == 0 {
mapping = append(mapping, APIPort{
PublicPort: int64(p),
Type: port.Proto(),
})
continue
}
for _, binding := range bindings {
p, _ := parsePort(port.Port())
h, _ := parsePort(binding.HostPort)
mapping = append(mapping, APIPort{
PrivatePort: int64(p),
PublicPort: int64(h),
Type: port.Proto(),
IP: binding.HostIp,
})
}
}
return mapping
}
func parsePort(rawPort string) (int, error) {
port, err := strconv.ParseUint(rawPort, 10, 16)
if err != nil {
return 0, err
}
return int(port), nil
}
type Config struct {
Hostname string
Domainname string
User string
Memory int64
MemorySwap int64
CpuShares int64
AttachStdin bool
AttachStdout bool
AttachStderr bool
PortSpecs []string
ExposedPorts map[Port]struct{}
Tty bool
OpenStdin bool
StdinOnce bool
Env []string
Cmd []string
Dns []string // For Docker API v1.9 and below only
Image string
Volumes map[string]struct{}
VolumesFrom string
WorkingDir string
Entrypoint []string
NetworkDisabled bool
}
type Container struct {
ID string
Created time.Time
Path string
Args []string
Config *Config
State State
Image string
NetworkSettings *NetworkSettings
SysInitPath string
ResolvConfPath string
HostnamePath string
HostsPath string
Name string
Driver string
Volumes map[string]string
VolumesRW map[string]bool
HostConfig *HostConfig
}
// InspectContainer returns information about a container by its ID.
//
// See http://goo.gl/2o52Sx for more details.
func (c *Client) InspectContainer(id string) (*Container, error) {
path := "/containers/" + id + "/json"
body, status, err := c.do("GET", path, nil)
if status == http.StatusNotFound {
return nil, &NoSuchContainer{ID: id}
}
if err != nil {
return nil, err
}
var container Container
err = json.Unmarshal(body, &container)
if err != nil {
return nil, err
}
return &container, nil
}
// ContainerChanges returns changes in the filesystem of the given container.
//
// See http://goo.gl/DpGyzK for more details.
func (c *Client) ContainerChanges(id string) ([]Change, error) {
path := "/containers/" + id + "/changes"
body, status, err := c.do("GET", path, nil)
if status == http.StatusNotFound {
return nil, &NoSuchContainer{ID: id}
}
if err != nil {
return nil, err
}
var changes []Change
err = json.Unmarshal(body, &changes)
if err != nil {
return nil, err
}
return changes, nil
}
// CreateContainerOptions specify parameters to the CreateContainer function.
//
// See http://goo.gl/WPPYtB for more details.
type CreateContainerOptions struct {
Name string
Config *Config `qs:"-"`
}
// CreateContainer creates a new container, returning the container instance,
// or an error in case of failure.
//
// See http://goo.gl/tjihUc for more details.
func (c *Client) CreateContainer(opts CreateContainerOptions) (*Container, error) {
path := "/containers/create?" + queryString(opts)
body, status, err := c.do("POST", path, opts.Config)
if status == http.StatusNotFound {
return nil, ErrNoSuchImage
}
if err != nil {
return nil, err
}
var container Container
err = json.Unmarshal(body, &container)
if err != nil {
return nil, err
}
container.Name = opts.Name
return &container, nil
}
type KeyValuePair struct {
Key string
Value string
}
type HostConfig struct {
Binds []string
ContainerIDFile string
LxcConf []KeyValuePair
Privileged bool
PortBindings map[Port][]PortBinding
Links []string
PublishAllPorts bool
Dns []string // For Docker API v1.10 and above only
}
// StartContainer starts a container, returning an errror in case of failure.
//
// See http://goo.gl/y5GZlE for more details.
func (c *Client) StartContainer(id string, hostConfig *HostConfig) error {
if hostConfig == nil {
hostConfig = &HostConfig{}
}
path := "/containers/" + id + "/start"
_, status, err := c.do("POST", path, hostConfig)
if status == http.StatusNotFound {
return &NoSuchContainer{ID: id}
}
if err != nil {
return err
}
return nil
}
// StopContainer stops a container, killing it after the given timeout (in
// seconds).
//
// See http://goo.gl/X2mj8t for more details.
func (c *Client) StopContainer(id string, timeout uint) error {
path := fmt.Sprintf("/containers/%s/stop?t=%d", id, timeout)
_, status, err := c.do("POST", path, nil)
if status == http.StatusNotFound {
return &NoSuchContainer{ID: id}
}
if err != nil {
return err
}
return nil
}
// RestartContainer stops a container, killing it after the given timeout (in
// seconds), during the stop process.
//
// See http://goo.gl/zms73Z for more details.
func (c *Client) RestartContainer(id string, timeout uint) error {
path := fmt.Sprintf("/containers/%s/restart?t=%d", id, timeout)
_, status, err := c.do("POST", path, nil)
if status == http.StatusNotFound {
return &NoSuchContainer{ID: id}
}
if err != nil {
return err
}
return nil
}
// KillContainerOptions represents the set of options that can be used in a
// call to KillContainer.
type KillContainerOptions struct {
// The ID of the container.
ID string `qs:"-"`
// The signal to send to the container. When omitted, Docker server
// will assume SIGKILL.
Signal Signal
}
// KillContainer kills a container, returning an error in case of failure.
//
// See http://goo.gl/DPbbBy for more details.
func (c *Client) KillContainer(opts KillContainerOptions) error {
path := "/containers/" + opts.ID + "/kill" + "?" + queryString(opts)
_, status, err := c.do("POST", path, nil)
if status == http.StatusNotFound {
return &NoSuchContainer{ID: opts.ID}
}
if err != nil {
return err
}
return nil
}
// RemoveContainerOptions encapsulates options to remove a container.
type RemoveContainerOptions struct {
// The ID of the container.
ID string `qs:"-"`
// A flag that indicates whether Docker should remove the volumes
// associated to the container.
RemoveVolumes bool `qs:"v"`
// A flag that indicates whether Docker should remove the container
// even if it is currently running.
Force bool
}
// RemoveContainer removes a container, returning an error in case of failure.
//
// See http://goo.gl/PBvGdU for more details.
func (c *Client) RemoveContainer(opts RemoveContainerOptions) error {
path := "/containers/" + opts.ID + "?" + queryString(opts)
_, status, err := c.do("DELETE", path, nil)
if status == http.StatusNotFound {
return &NoSuchContainer{ID: opts.ID}
}
if err != nil {
return err
}
return nil
}
// CopyFromContainerOptions is the set of options that can be used when copying
// files or folders from a container.
//
// See http://goo.gl/mnxRMl for more details.
type CopyFromContainerOptions struct {
OutputStream io.Writer `json:"-"`
Container string `json:"-"`
Resource string
}
// CopyFromContainer copy files or folders from a container, using a given
// resource.
//
// See http://goo.gl/mnxRMl for more details.
func (c *Client) CopyFromContainer(opts CopyFromContainerOptions) error {
if opts.Container == "" {
return &NoSuchContainer{ID: opts.Container}
}
url := fmt.Sprintf("/containers/%s/copy", opts.Container)
body, status, err := c.do("POST", url, opts)
if status == http.StatusNotFound {
return &NoSuchContainer{ID: opts.Container}
}
if err != nil {
return err
}
io.Copy(opts.OutputStream, bytes.NewBuffer(body))
return nil
}
// WaitContainer blocks until the given container stops, return the exit code
// of the container status.
//
// See http://goo.gl/gnHJL2 for more details.
func (c *Client) WaitContainer(id string) (int, error) {
body, status, err := c.do("POST", "/containers/"+id+"/wait", nil)
if status == http.StatusNotFound {
return 0, &NoSuchContainer{ID: id}
}
if err != nil {
return 0, err
}
var r struct{ StatusCode int }
err = json.Unmarshal(body, &r)
if err != nil {
return 0, err
}
return r.StatusCode, nil
}
// CommitContainerOptions aggregates parameters to the CommitContainer method.
//
// See http://goo.gl/628gxm for more details.
type CommitContainerOptions struct {
Container string
Repository string `qs:"repo"`
Tag string
Message string `qs:"m"`
Author string
Run *Config `qs:"-"`
}
type Image struct {
ID string `json:"id"`
Parent string `json:"parent,omitempty"`
Comment string `json:"comment,omitempty"`
Created time.Time `json:"created"`
Container string `json:"container,omitempty"`
ContainerConfig Config `json:"container_config,omitempty"`
DockerVersion string `json:"docker_version,omitempty"`
Author string `json:"author,omitempty"`
Config *Config `json:"config,omitempty"`
Architecture string `json:"architecture,omitempty"`
Size int64
}
// CommitContainer creates a new image from a container's changes.
//
// See http://goo.gl/628gxm for more details.
func (c *Client) CommitContainer(opts CommitContainerOptions) (*Image, error) {
path := "/commit?" + queryString(opts)
body, status, err := c.do("POST", path, opts.Run)
if status == http.StatusNotFound {
return nil, &NoSuchContainer{ID: opts.Container}
}
if err != nil {
return nil, err
}
var image Image
err = json.Unmarshal(body, &image)
if err != nil {
return nil, err
}
return &image, nil
}
// AttachToContainerOptions is the set of options that can be used when
// attaching to a container.
//
// See http://goo.gl/oPzcqH for more details.
type AttachToContainerOptions struct {
Container string `qs:"-"`
InputStream io.Reader `qs:"-"`
OutputStream io.Writer `qs:"-"`
ErrorStream io.Writer `qs:"-"`
// Get container logs, sending it to OutputStream.
Logs bool
// Stream the response?
Stream bool
// Attach to stdin, and use InputFile.
Stdin bool
// Attach to stdout, and use OutputStream.
Stdout bool
// Attach to stderr, and use ErrorStream.
Stderr bool
// If set, after a successful connect, a sentinel will be sent and then the
// client will block on receive before continuing.
//
// It must be an unbuffered channel. Using a buffered channel can lead
// to unexpected behavior.
Success chan struct{}
}
// AttachToContainer attaches to a container, using the given options.
//
// See http://goo.gl/oPzcqH for more details.
func (c *Client) AttachToContainer(opts AttachToContainerOptions) error {
if opts.Container == "" {
return &NoSuchContainer{ID: opts.Container}
}
path := "/containers/" + opts.Container + "/attach?" + queryString(opts)
return c.hijack("POST", path, opts.Success, opts.InputStream, opts.ErrorStream, opts.OutputStream)
}
// ResizeContainerTTY resizes the terminal to the given height and width.
func (c *Client) ResizeContainerTTY(id string, height, width int) error {
params := make(url.Values)
params.Set("h", strconv.Itoa(height))
params.Set("w", strconv.Itoa(width))
_, _, err := c.do("POST", "/containers/"+id+"/resize?"+params.Encode(), nil)
return err
}
// ExportContainerOptions is the set of parameters to the ExportContainer
// method.
//
// See http://goo.gl/Lqk0FZ for more details.
type ExportContainerOptions struct {
ID string
OutputStream io.Writer
}
// ExportContainer export the contents of container id as tar archive
// and prints the exported contents to stdout.
//
// See http://goo.gl/Lqk0FZ for more details.
func (c *Client) ExportContainer(opts ExportContainerOptions) error {
if opts.ID == "" {
return NoSuchContainer{ID: opts.ID}
}
url := fmt.Sprintf("/containers/%s/export", opts.ID)
return c.stream("GET", url, nil, nil, opts.OutputStream)
}
// NoSuchContainer is the error returned when a given container does not exist.
type NoSuchContainer struct {
ID string
}
func (err NoSuchContainer) Error() string {
return "No such container: " + err.ID
}

View File

@@ -0,0 +1,888 @@
// Copyright 2014 go-dockerclient authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package docker
import (
"bytes"
"encoding/json"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"reflect"
"runtime"
"strconv"
"strings"
"testing"
)
func TestListContainers(t *testing.T) {
jsonContainers := `[
{
"Id": "8dfafdbc3a40",
"Image": "base:latest",
"Command": "echo 1",
"Created": 1367854155,
"Ports":[{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}],
"Status": "Exit 0"
},
{
"Id": "9cd87474be90",
"Image": "base:latest",
"Command": "echo 222222",
"Created": 1367854155,
"Ports":[{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}],
"Status": "Exit 0"
},
{
"Id": "3176a2479c92",
"Image": "base:latest",
"Command": "echo 3333333333333333",
"Created": 1367854154,
"Ports":[{"PrivatePort": 2221, "PublicPort": 3331, "Type": "tcp"}],
"Status": "Exit 0"
},
{
"Id": "4cb07b47f9fb",
"Image": "base:latest",
"Command": "echo 444444444444444444444444444444444",
"Ports":[{"PrivatePort": 2223, "PublicPort": 3332, "Type": "tcp"}],
"Created": 1367854152,
"Status": "Exit 0"
}
]`
var expected []APIContainers
err := json.Unmarshal([]byte(jsonContainers), &expected)
if err != nil {
t.Fatal(err)
}
client := newTestClient(&FakeRoundTripper{message: jsonContainers, status: http.StatusOK})
containers, err := client.ListContainers(ListContainersOptions{})
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(containers, expected) {
t.Errorf("ListContainers: Expected %#v. Got %#v.", expected, containers)
}
}
func TestListContainersParams(t *testing.T) {
var tests = []struct {
input ListContainersOptions
params map[string][]string
}{
{ListContainersOptions{}, map[string][]string{}},
{ListContainersOptions{All: true}, map[string][]string{"all": {"1"}}},
{ListContainersOptions{All: true, Limit: 10}, map[string][]string{"all": {"1"}, "limit": {"10"}}},
{
ListContainersOptions{All: true, Limit: 10, Since: "adf9983", Before: "abdeef"},
map[string][]string{"all": {"1"}, "limit": {"10"}, "since": {"adf9983"}, "before": {"abdeef"}},
},
}
fakeRT := &FakeRoundTripper{message: "[]", status: http.StatusOK}
client := newTestClient(fakeRT)
u, _ := url.Parse(client.getURL("/containers/json"))
for _, tt := range tests {
client.ListContainers(tt.input)
got := map[string][]string(fakeRT.requests[0].URL.Query())
if !reflect.DeepEqual(got, tt.params) {
t.Errorf("Expected %#v, got %#v.", tt.params, got)
}
if path := fakeRT.requests[0].URL.Path; path != u.Path {
t.Errorf("Wrong path on request. Want %q. Got %q.", u.Path, path)
}
if meth := fakeRT.requests[0].Method; meth != "GET" {
t.Errorf("Wrong HTTP method. Want GET. Got %s.", meth)
}
fakeRT.Reset()
}
}
func TestListContainersFailure(t *testing.T) {
var tests = []struct {
status int
message string
}{
{400, "bad parameter"},
{500, "internal server error"},
}
for _, tt := range tests {
client := newTestClient(&FakeRoundTripper{message: tt.message, status: tt.status})
expected := Error{Status: tt.status, Message: tt.message}
containers, err := client.ListContainers(ListContainersOptions{})
if !reflect.DeepEqual(expected, *err.(*Error)) {
t.Errorf("Wrong error in ListContainers. Want %#v. Got %#v.", expected, err)
}
if len(containers) > 0 {
t.Errorf("ListContainers failure. Expected empty list. Got %#v.", containers)
}
}
}
func TestInspectContainer(t *testing.T) {
jsonContainer := `{
"Id": "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2",
"Created": "2013-05-07T14:51:42.087658+02:00",
"Path": "date",
"Args": [],
"Config": {
"Hostname": "4fa6e0f0c678",
"User": "",
"Memory": 0,
"MemorySwap": 0,
"AttachStdin": false,
"AttachStdout": true,
"AttachStderr": true,
"PortSpecs": null,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": null,
"Cmd": [
"date"
],
"Image": "base",
"Volumes": {},
"VolumesFrom": ""
},
"State": {
"Running": false,
"Pid": 0,
"ExitCode": 0,
"StartedAt": "2013-05-07T14:51:42.087658+02:00",
"Ghost": false
},
"Image": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc",
"NetworkSettings": {
"IpAddress": "",
"IpPrefixLen": 0,
"Gateway": "",
"Bridge": "",
"PortMapping": null
},
"SysInitPath": "/home/kitty/go/src/github.com/dotcloud/docker/bin/docker",
"ResolvConfPath": "/etc/resolv.conf",
"Volumes": {},
"HostConfig": {
"Binds": null,
"ContainerIDFile": "",
"LxcConf": [],
"Privileged": false,
"PortBindings": {
"80/tcp": [
{
"HostIp": "0.0.0.0",
"HostPort": "49153"
}
]
},
"Links": null,
"PublishAllPorts": false
}
}`
var expected Container
err := json.Unmarshal([]byte(jsonContainer), &expected)
if err != nil {
t.Fatal(err)
}
fakeRT := &FakeRoundTripper{message: jsonContainer, status: http.StatusOK}
client := newTestClient(fakeRT)
id := "4fa6e0f0c678"
container, err := client.InspectContainer(id)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(*container, expected) {
t.Errorf("InspectContainer(%q): Expected %#v. Got %#v.", id, expected, container)
}
expectedURL, _ := url.Parse(client.getURL("/containers/4fa6e0f0c678/json"))
if gotPath := fakeRT.requests[0].URL.Path; gotPath != expectedURL.Path {
t.Errorf("InspectContainer(%q): Wrong path in request. Want %q. Got %q.", id, expectedURL.Path, gotPath)
}
}
func TestInspectContainerFailure(t *testing.T) {
client := newTestClient(&FakeRoundTripper{message: "server error", status: 500})
expected := Error{Status: 500, Message: "server error"}
container, err := client.InspectContainer("abe033")
if container != nil {
t.Errorf("InspectContainer: Expected <nil> container, got %#v", container)
}
if !reflect.DeepEqual(expected, *err.(*Error)) {
t.Errorf("InspectContainer: Wrong error information. Want %#v. Got %#v.", expected, err)
}
}
func TestInspectContainerNotFound(t *testing.T) {
client := newTestClient(&FakeRoundTripper{message: "no such container", status: 404})
container, err := client.InspectContainer("abe033")
if container != nil {
t.Errorf("InspectContainer: Expected <nil> container, got %#v", container)
}
expected := &NoSuchContainer{ID: "abe033"}
if !reflect.DeepEqual(err, expected) {
t.Errorf("InspectContainer: Wrong error information. Want %#v. Got %#v.", expected, err)
}
}
func TestContainerChanges(t *testing.T) {
jsonChanges := `[
{
"Path":"/dev",
"Kind":0
},
{
"Path":"/dev/kmsg",
"Kind":1
},
{
"Path":"/test",
"Kind":1
}
]`
var expected []Change
err := json.Unmarshal([]byte(jsonChanges), &expected)
if err != nil {
t.Fatal(err)
}
fakeRT := &FakeRoundTripper{message: jsonChanges, status: http.StatusOK}
client := newTestClient(fakeRT)
id := "4fa6e0f0c678"
changes, err := client.ContainerChanges(id)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(changes, expected) {
t.Errorf("ContainerChanges(%q): Expected %#v. Got %#v.", id, expected, changes)
}
expectedURL, _ := url.Parse(client.getURL("/containers/4fa6e0f0c678/changes"))
if gotPath := fakeRT.requests[0].URL.Path; gotPath != expectedURL.Path {
t.Errorf("ContainerChanges(%q): Wrong path in request. Want %q. Got %q.", id, expectedURL.Path, gotPath)
}
}
func TestContainerChangesFailure(t *testing.T) {
client := newTestClient(&FakeRoundTripper{message: "server error", status: 500})
expected := Error{Status: 500, Message: "server error"}
changes, err := client.ContainerChanges("abe033")
if changes != nil {
t.Errorf("ContainerChanges: Expected <nil> changes, got %#v", changes)
}
if !reflect.DeepEqual(expected, *err.(*Error)) {
t.Errorf("ContainerChanges: Wrong error information. Want %#v. Got %#v.", expected, err)
}
}
func TestContainerChangesNotFound(t *testing.T) {
client := newTestClient(&FakeRoundTripper{message: "no such container", status: 404})
changes, err := client.ContainerChanges("abe033")
if changes != nil {
t.Errorf("ContainerChanges: Expected <nil> changes, got %#v", changes)
}
expected := &NoSuchContainer{ID: "abe033"}
if !reflect.DeepEqual(err, expected) {
t.Errorf("ContainerChanges: Wrong error information. Want %#v. Got %#v.", expected, err)
}
}
func TestCreateContainer(t *testing.T) {
jsonContainer := `{
"Id": "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2",
"Warnings": []
}`
var expected Container
err := json.Unmarshal([]byte(jsonContainer), &expected)
if err != nil {
t.Fatal(err)
}
fakeRT := &FakeRoundTripper{message: jsonContainer, status: http.StatusOK}
client := newTestClient(fakeRT)
config := Config{AttachStdout: true, AttachStdin: true}
opts := CreateContainerOptions{Name: "TestCreateContainer", Config: &config}
container, err := client.CreateContainer(opts)
if err != nil {
t.Fatal(err)
}
id := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2"
if container.ID != id {
t.Errorf("CreateContainer: wrong ID. Want %q. Got %q.", id, container.ID)
}
req := fakeRT.requests[0]
if req.Method != "POST" {
t.Errorf("CreateContainer: wrong HTTP method. Want %q. Got %q.", "POST", req.Method)
}
expectedURL, _ := url.Parse(client.getURL("/containers/create"))
if gotPath := req.URL.Path; gotPath != expectedURL.Path {
t.Errorf("CreateContainer: Wrong path in request. Want %q. Got %q.", expectedURL.Path, gotPath)
}
var gotBody Config
err = json.NewDecoder(req.Body).Decode(&gotBody)
if err != nil {
t.Fatal(err)
}
}
func TestCreateContainerImageNotFound(t *testing.T) {
client := newTestClient(&FakeRoundTripper{message: "No such image", status: http.StatusNotFound})
config := Config{AttachStdout: true, AttachStdin: true}
container, err := client.CreateContainer(CreateContainerOptions{Config: &config})
if container != nil {
t.Errorf("CreateContainer: expected <nil> container, got %#v.", container)
}
if !reflect.DeepEqual(err, ErrNoSuchImage) {
t.Errorf("CreateContainer: Wrong error type. Want %#v. Got %#v.", ErrNoSuchImage, err)
}
}
func TestStartContainer(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK}
client := newTestClient(fakeRT)
id := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2"
err := client.StartContainer(id, &HostConfig{})
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
if req.Method != "POST" {
t.Errorf("StartContainer(%q): wrong HTTP method. Want %q. Got %q.", id, "POST", req.Method)
}
expectedURL, _ := url.Parse(client.getURL("/containers/" + id + "/start"))
if gotPath := req.URL.Path; gotPath != expectedURL.Path {
t.Errorf("StartContainer(%q): Wrong path in request. Want %q. Got %q.", id, expectedURL.Path, gotPath)
}
expectedContentType := "application/json"
if contentType := req.Header.Get("Content-Type"); contentType != expectedContentType {
t.Errorf("StartContainer(%q): Wrong content-type in request. Want %q. Got %q.", id, expectedContentType, contentType)
}
}
func TestStartContainerNilHostConfig(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK}
client := newTestClient(fakeRT)
id := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2"
err := client.StartContainer(id, nil)
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
if req.Method != "POST" {
t.Errorf("StartContainer(%q): wrong HTTP method. Want %q. Got %q.", id, "POST", req.Method)
}
expectedURL, _ := url.Parse(client.getURL("/containers/" + id + "/start"))
if gotPath := req.URL.Path; gotPath != expectedURL.Path {
t.Errorf("StartContainer(%q): Wrong path in request. Want %q. Got %q.", id, expectedURL.Path, gotPath)
}
expectedContentType := "application/json"
if contentType := req.Header.Get("Content-Type"); contentType != expectedContentType {
t.Errorf("StartContainer(%q): Wrong content-type in request. Want %q. Got %q.", id, expectedContentType, contentType)
}
}
func TestStartContainerNotFound(t *testing.T) {
client := newTestClient(&FakeRoundTripper{message: "no such container", status: http.StatusNotFound})
err := client.StartContainer("a2344", &HostConfig{})
expected := &NoSuchContainer{ID: "a2344"}
if !reflect.DeepEqual(err, expected) {
t.Errorf("StartContainer: Wrong error returned. Want %#v. Got %#v.", expected, err)
}
}
func TestStopContainer(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusNoContent}
client := newTestClient(fakeRT)
id := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2"
err := client.StopContainer(id, 10)
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
if req.Method != "POST" {
t.Errorf("StopContainer(%q, 10): wrong HTTP method. Want %q. Got %q.", id, "POST", req.Method)
}
expectedURL, _ := url.Parse(client.getURL("/containers/" + id + "/stop"))
if gotPath := req.URL.Path; gotPath != expectedURL.Path {
t.Errorf("StopContainer(%q, 10): Wrong path in request. Want %q. Got %q.", id, expectedURL.Path, gotPath)
}
}
func TestStopContainerNotFound(t *testing.T) {
client := newTestClient(&FakeRoundTripper{message: "no such container", status: http.StatusNotFound})
err := client.StopContainer("a2334", 10)
expected := &NoSuchContainer{ID: "a2334"}
if !reflect.DeepEqual(err, expected) {
t.Errorf("StopContainer: Wrong error returned. Want %#v. Got %#v.", expected, err)
}
}
func TestRestartContainer(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusNoContent}
client := newTestClient(fakeRT)
id := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2"
err := client.RestartContainer(id, 10)
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
if req.Method != "POST" {
t.Errorf("RestartContainer(%q, 10): wrong HTTP method. Want %q. Got %q.", id, "POST", req.Method)
}
expectedURL, _ := url.Parse(client.getURL("/containers/" + id + "/restart"))
if gotPath := req.URL.Path; gotPath != expectedURL.Path {
t.Errorf("RestartContainer(%q, 10): Wrong path in request. Want %q. Got %q.", id, expectedURL.Path, gotPath)
}
}
func TestRestartContainerNotFound(t *testing.T) {
client := newTestClient(&FakeRoundTripper{message: "no such container", status: http.StatusNotFound})
err := client.RestartContainer("a2334", 10)
expected := &NoSuchContainer{ID: "a2334"}
if !reflect.DeepEqual(err, expected) {
t.Errorf("RestartContainer: Wrong error returned. Want %#v. Got %#v.", expected, err)
}
}
func TestKillContainer(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusNoContent}
client := newTestClient(fakeRT)
id := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2"
err := client.KillContainer(KillContainerOptions{ID: id})
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
if req.Method != "POST" {
t.Errorf("KillContainer(%q): wrong HTTP method. Want %q. Got %q.", id, "POST", req.Method)
}
expectedURL, _ := url.Parse(client.getURL("/containers/" + id + "/kill"))
if gotPath := req.URL.Path; gotPath != expectedURL.Path {
t.Errorf("KillContainer(%q): Wrong path in request. Want %q. Got %q.", id, expectedURL.Path, gotPath)
}
}
func TestKillContainerSignal(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusNoContent}
client := newTestClient(fakeRT)
id := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2"
err := client.KillContainer(KillContainerOptions{ID: id, Signal: SIGTERM})
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
if req.Method != "POST" {
t.Errorf("KillContainer(%q): wrong HTTP method. Want %q. Got %q.", id, "POST", req.Method)
}
if signal := req.URL.Query().Get("signal"); signal != "15" {
t.Errorf("KillContainer(%q): Wrong query string in request. Want %q. Got %q.", id, "15", signal)
}
}
func TestKillContainerNotFound(t *testing.T) {
client := newTestClient(&FakeRoundTripper{message: "no such container", status: http.StatusNotFound})
err := client.KillContainer(KillContainerOptions{ID: "a2334"})
expected := &NoSuchContainer{ID: "a2334"}
if !reflect.DeepEqual(err, expected) {
t.Errorf("KillContainer: Wrong error returned. Want %#v. Got %#v.", expected, err)
}
}
func TestRemoveContainer(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK}
client := newTestClient(fakeRT)
id := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2"
opts := RemoveContainerOptions{ID: id}
err := client.RemoveContainer(opts)
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
if req.Method != "DELETE" {
t.Errorf("RemoveContainer(%q): wrong HTTP method. Want %q. Got %q.", id, "DELETE", req.Method)
}
expectedURL, _ := url.Parse(client.getURL("/containers/" + id))
if gotPath := req.URL.Path; gotPath != expectedURL.Path {
t.Errorf("RemoveContainer(%q): Wrong path in request. Want %q. Got %q.", id, expectedURL.Path, gotPath)
}
}
func TestRemoveContainerRemoveVolumes(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK}
client := newTestClient(fakeRT)
id := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2"
opts := RemoveContainerOptions{ID: id, RemoveVolumes: true}
err := client.RemoveContainer(opts)
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
params := map[string][]string(req.URL.Query())
expected := map[string][]string{"v": {"1"}}
if !reflect.DeepEqual(params, expected) {
t.Errorf("RemoveContainer(%q): wrong parameters. Want %#v. Got %#v.", id, expected, params)
}
}
func TestRemoveContainerNotFound(t *testing.T) {
client := newTestClient(&FakeRoundTripper{message: "no such container", status: http.StatusNotFound})
err := client.RemoveContainer(RemoveContainerOptions{ID: "a2334"})
expected := &NoSuchContainer{ID: "a2334"}
if !reflect.DeepEqual(err, expected) {
t.Errorf("RemoveContainer: Wrong error returned. Want %#v. Got %#v.", expected, err)
}
}
func TestResizeContainerTTY(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK}
client := newTestClient(fakeRT)
id := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2"
err := client.ResizeContainerTTY(id, 40, 80)
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
if req.Method != "POST" {
t.Errorf("ResizeContainerTTY(%q): wrong HTTP method. Want %q. Got %q.", id, "POST", req.Method)
}
expectedURL, _ := url.Parse(client.getURL("/containers/" + id + "/resize"))
if gotPath := req.URL.Path; gotPath != expectedURL.Path {
t.Errorf("ResizeContainerTTY(%q): Wrong path in request. Want %q. Got %q.", id, expectedURL.Path, gotPath)
}
got := map[string][]string(req.URL.Query())
expectedParams := map[string][]string{
"w": {"80"},
"h": {"40"},
}
if !reflect.DeepEqual(got, expectedParams) {
t.Errorf("Expected %#v, got %#v.", expectedParams, got)
}
}
func TestWaitContainer(t *testing.T) {
fakeRT := &FakeRoundTripper{message: `{"StatusCode": 56}`, status: http.StatusOK}
client := newTestClient(fakeRT)
id := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2"
status, err := client.WaitContainer(id)
if err != nil {
t.Fatal(err)
}
if status != 56 {
t.Errorf("WaitContainer(%q): wrong return. Want 56. Got %d.", id, status)
}
req := fakeRT.requests[0]
if req.Method != "POST" {
t.Errorf("WaitContainer(%q): wrong HTTP method. Want %q. Got %q.", id, "POST", req.Method)
}
expectedURL, _ := url.Parse(client.getURL("/containers/" + id + "/wait"))
if gotPath := req.URL.Path; gotPath != expectedURL.Path {
t.Errorf("WaitContainer(%q): Wrong path in request. Want %q. Got %q.", id, expectedURL.Path, gotPath)
}
}
func TestWaitContainerNotFound(t *testing.T) {
client := newTestClient(&FakeRoundTripper{message: "no such container", status: http.StatusNotFound})
_, err := client.WaitContainer("a2334")
expected := &NoSuchContainer{ID: "a2334"}
if !reflect.DeepEqual(err, expected) {
t.Errorf("WaitContainer: Wrong error returned. Want %#v. Got %#v.", expected, err)
}
}
func TestCommitContainer(t *testing.T) {
response := `{"Id":"596069db4bf5"}`
client := newTestClient(&FakeRoundTripper{message: response, status: http.StatusOK})
id := "596069db4bf5"
image, err := client.CommitContainer(CommitContainerOptions{})
if err != nil {
t.Fatal(err)
}
if image.ID != id {
t.Errorf("CommitContainer: Wrong image id. Want %q. Got %q.", id, image.ID)
}
}
func TestCommitContainerParams(t *testing.T) {
cfg := Config{Memory: 67108864}
json, _ := json.Marshal(&cfg)
var tests = []struct {
input CommitContainerOptions
params map[string][]string
body []byte
}{
{CommitContainerOptions{}, map[string][]string{}, nil},
{CommitContainerOptions{Container: "44c004db4b17"}, map[string][]string{"container": {"44c004db4b17"}}, nil},
{
CommitContainerOptions{Container: "44c004db4b17", Repository: "tsuru/python", Message: "something"},
map[string][]string{"container": {"44c004db4b17"}, "repo": {"tsuru/python"}, "m": {"something"}},
nil,
},
{
CommitContainerOptions{Container: "44c004db4b17", Run: &cfg},
map[string][]string{"container": {"44c004db4b17"}},
json,
},
}
fakeRT := &FakeRoundTripper{message: "[]", status: http.StatusOK}
client := newTestClient(fakeRT)
u, _ := url.Parse(client.getURL("/commit"))
for _, tt := range tests {
client.CommitContainer(tt.input)
got := map[string][]string(fakeRT.requests[0].URL.Query())
if !reflect.DeepEqual(got, tt.params) {
t.Errorf("Expected %#v, got %#v.", tt.params, got)
}
if path := fakeRT.requests[0].URL.Path; path != u.Path {
t.Errorf("Wrong path on request. Want %q. Got %q.", u.Path, path)
}
if meth := fakeRT.requests[0].Method; meth != "POST" {
t.Errorf("Wrong HTTP method. Want POST. Got %s.", meth)
}
if tt.body != nil {
if requestBody, err := ioutil.ReadAll(fakeRT.requests[0].Body); err == nil {
if bytes.Compare(requestBody, tt.body) != 0 {
t.Errorf("Expected body %#v, got %#v", tt.body, requestBody)
}
} else {
t.Errorf("Error reading request body: %#v", err)
}
}
fakeRT.Reset()
}
}
func TestCommitContainerFailure(t *testing.T) {
client := newTestClient(&FakeRoundTripper{message: "no such container", status: http.StatusInternalServerError})
_, err := client.CommitContainer(CommitContainerOptions{})
if err == nil {
t.Error("Expected non-nil error, got <nil>.")
}
}
func TestCommitContainerNotFound(t *testing.T) {
client := newTestClient(&FakeRoundTripper{message: "no such container", status: http.StatusNotFound})
_, err := client.CommitContainer(CommitContainerOptions{})
expected := &NoSuchContainer{ID: ""}
if !reflect.DeepEqual(err, expected) {
t.Errorf("CommitContainer: Wrong error returned. Want %#v. Got %#v.", expected, err)
}
}
func TestAttachToContainerLogs(t *testing.T) {
var req http.Request
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte{1, 0, 0, 0, 0, 0, 0, 19})
w.Write([]byte("something happened!"))
req = *r
}))
defer server.Close()
client, _ := NewClient(server.URL)
var buf bytes.Buffer
opts := AttachToContainerOptions{
Container: "a123456",
OutputStream: &buf,
Stdout: true,
Stderr: true,
Logs: true,
}
err := client.AttachToContainer(opts)
if err != nil {
t.Fatal(err)
}
expected := "something happened!"
if buf.String() != expected {
t.Errorf("AttachToContainer for logs: wrong output. Want %q. Got %q.", expected, buf.String())
}
if req.Method != "POST" {
t.Errorf("AttachToContainer: wrong HTTP method. Want POST. Got %s.", req.Method)
}
u, _ := url.Parse(client.getURL("/containers/a123456/attach"))
if req.URL.Path != u.Path {
t.Errorf("AttachToContainer for logs: wrong HTTP path. Want %q. Got %q.", u.Path, req.URL.Path)
}
expectedQs := map[string][]string{
"logs": {"1"},
"stdout": {"1"},
"stderr": {"1"},
}
got := map[string][]string(req.URL.Query())
if !reflect.DeepEqual(got, expectedQs) {
t.Errorf("AttachToContainer: wrong query string. Want %#v. Got %#v.", expectedQs, got)
}
}
func TestAttachToContainer(t *testing.T) {
var reader = strings.NewReader("send value")
var req http.Request
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte{1, 0, 0, 0, 0, 0, 0, 5})
w.Write([]byte("hello"))
req = *r
}))
defer server.Close()
client, _ := NewClient(server.URL)
var stdout, stderr bytes.Buffer
opts := AttachToContainerOptions{
Container: "a123456",
OutputStream: &stdout,
ErrorStream: &stderr,
InputStream: reader,
Stdin: true,
Stdout: true,
Stderr: true,
Stream: true,
}
var err = client.AttachToContainer(opts)
if err != nil {
t.Fatal(err)
}
expected := map[string][]string{
"stdin": {"1"},
"stdout": {"1"},
"stderr": {"1"},
"stream": {"1"},
}
got := map[string][]string(req.URL.Query())
if !reflect.DeepEqual(got, expected) {
t.Errorf("AttachToContainer: wrong query string. Want %#v. Got %#v.", expected, got)
}
}
func TestAttachToContainerWithoutContainer(t *testing.T) {
var client Client
err := client.AttachToContainer(AttachToContainerOptions{})
expected := &NoSuchContainer{ID: ""}
if !reflect.DeepEqual(err, expected) {
t.Errorf("AttachToContainer: wrong error. Want %#v. Got %#v.", expected, err)
}
}
func TestNoSuchContainerError(t *testing.T) {
var err error = &NoSuchContainer{ID: "i345"}
expected := "No such container: i345"
if got := err.Error(); got != expected {
t.Errorf("NoSuchContainer: wrong message. Want %q. Got %q.", expected, got)
}
}
func TestExportContainer(t *testing.T) {
content := "exported container tar content"
out := stdoutMock{bytes.NewBufferString(content)}
client := newTestClient(&FakeRoundTripper{status: http.StatusOK})
opts := ExportContainerOptions{ID: "4fa6e0f0c678", OutputStream: out}
err := client.ExportContainer(opts)
if err != nil {
t.Errorf("ExportContainer: caugh error %#v while exporting container, expected nil", err.Error())
}
if out.String() != content {
t.Errorf("ExportContainer: wrong stdout. Want %#v. Got %#v.", content, out.String())
}
}
func TestExportContainerViaUnixSocket(t *testing.T) {
if runtime.GOOS != "darwin" {
t.Skip("skipping test on %q", runtime.GOOS)
}
content := "exported container tar content"
var buf []byte
out := bytes.NewBuffer(buf)
tempSocket := tempfile("export_socket")
defer os.Remove(tempSocket)
endpoint := "unix://" + tempSocket
u, _ := parseEndpoint(endpoint)
client := Client{
endpoint: endpoint,
endpointURL: u,
client: http.DefaultClient,
}
listening := make(chan string)
done := make(chan int)
go runStreamConnServer(t, "unix", tempSocket, listening, done)
<-listening // wait for server to start
opts := ExportContainerOptions{ID: "4fa6e0f0c678", OutputStream: out}
err := client.ExportContainer(opts)
<-done // make sure server stopped
if err != nil {
t.Errorf("ExportContainer: caugh error %#v while exporting container, expected nil", err.Error())
}
if out.String() != content {
t.Errorf("ExportContainer: wrong stdout. Want %#v. Got %#v.", content, out.String())
}
}
func runStreamConnServer(t *testing.T, network, laddr string, listening chan<- string, done chan<- int) {
defer close(done)
l, err := net.Listen(network, laddr)
if err != nil {
t.Errorf("Listen(%q, %q) failed: %v", network, laddr, err)
listening <- "<nil>"
return
}
defer l.Close()
listening <- l.Addr().String()
c, err := l.Accept()
if err != nil {
t.Logf("Accept failed: %v", err)
return
}
c.Write([]byte("HTTP/1.1 200 OK\n\nexported container tar content"))
c.Close()
}
func tempfile(filename string) string {
return os.TempDir() + "/" + filename + "." + strconv.Itoa(os.Getpid())
}
func TestExportContainerNoId(t *testing.T) {
client := Client{}
out := stdoutMock{bytes.NewBufferString("")}
err := client.ExportContainer(ExportContainerOptions{OutputStream: out})
if err != (NoSuchContainer{}) {
t.Errorf("ExportContainer: wrong error. Want %#v. Got %#v.", NoSuchContainer{}, err)
}
}
func TestCopyFromContainer(t *testing.T) {
content := "File content"
out := stdoutMock{bytes.NewBufferString(content)}
client := newTestClient(&FakeRoundTripper{status: http.StatusOK})
opts := CopyFromContainerOptions{
Container: "a123456",
OutputStream: out,
}
err := client.CopyFromContainer(opts)
if err != nil {
t.Errorf("CopyFromContainer: caugh error %#v while copying from container, expected nil", err.Error())
}
if out.String() != content {
t.Errorf("CopyFromContainer: wrong stdout. Want %#v. Got %#v.", content, out.String())
}
}
func TestCopyFromContainerEmptyContainer(t *testing.T) {
client := newTestClient(&FakeRoundTripper{status: http.StatusOK})
err := client.CopyFromContainer(CopyFromContainerOptions{})
_, ok := err.(*NoSuchContainer)
if !ok {
t.Errorf("CopyFromContainer: invalid error returned. Want NoSuchContainer, got %#v.", err)
}
}
func TestPassingNameOptToCreateContainerReturnsItInContainer(t *testing.T) {
jsonContainer := `{
"Id": "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2",
"Warnings": []
}`
fakeRT := &FakeRoundTripper{message: jsonContainer, status: http.StatusOK}
client := newTestClient(fakeRT)
config := Config{AttachStdout: true, AttachStdin: true}
opts := CreateContainerOptions{Name: "TestCreateContainer", Config: &config}
container, err := client.CreateContainer(opts)
if err != nil {
t.Fatal(err)
}
if container.Name != "TestCreateContainer" {
t.Errorf("Container name expected to be TestCreateContainer, was %s", container.Name)
}
}

View File

@@ -0,0 +1,147 @@
// Copyright 2014 Docker authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the DOCKER-LICENSE file.
package engine
import (
"fmt"
"github.com/fsouza/go-dockerclient/utils"
"io"
"log"
"os"
"path/filepath"
"runtime"
"strings"
)
type Handler func(*Job) Status
var globalHandlers map[string]Handler
func init() {
globalHandlers = make(map[string]Handler)
}
func Register(name string, handler Handler) error {
_, exists := globalHandlers[name]
if exists {
return fmt.Errorf("Can't overwrite global handler for command %s", name)
}
globalHandlers[name] = handler
return nil
}
// The Engine is the core of Docker.
// It acts as a store for *containers*, and allows manipulation of these
// containers by executing *jobs*.
type Engine struct {
root string
handlers map[string]Handler
hack Hack // data for temporary hackery (see hack.go)
id string
Stdout io.Writer
Stderr io.Writer
Stdin io.Reader
}
func (eng *Engine) Root() string {
return eng.root
}
func (eng *Engine) Register(name string, handler Handler) error {
eng.Logf("Register(%s) (handlers=%v)", name, eng.handlers)
_, exists := eng.handlers[name]
if exists {
return fmt.Errorf("Can't overwrite handler for command %s", name)
}
eng.handlers[name] = handler
return nil
}
// New initializes a new engine managing the directory specified at `root`.
// `root` is used to store containers and any other state private to the engine.
// Changing the contents of the root without executing a job will cause unspecified
// behavior.
func New(root string) (*Engine, error) {
// Check for unsupported architectures
if runtime.GOARCH != "amd64" {
return nil, fmt.Errorf("The docker runtime currently only supports amd64 (not %s). This will change in the future. Aborting.", runtime.GOARCH)
}
// Check for unsupported kernel versions
// FIXME: it would be cleaner to not test for specific versions, but rather
// test for specific functionalities.
// Unfortunately we can't test for the feature "does not cause a kernel panic"
// without actually causing a kernel panic, so we need this workaround until
// the circumstances of pre-3.8 crashes are clearer.
// For details see http://github.com/dotcloud/docker/issues/407
if k, err := utils.GetKernelVersion(); err != nil {
log.Printf("WARNING: %s\n", err)
} else {
if utils.CompareKernelVersion(k, &utils.KernelVersionInfo{Kernel: 3, Major: 8, Minor: 0}) < 0 {
if os.Getenv("DOCKER_NOWARN_KERNEL_VERSION") == "" {
log.Printf("WARNING: You are running linux kernel version %s, which might be unstable running docker. Please upgrade your kernel to 3.8.0.", k.String())
}
}
}
if err := os.MkdirAll(root, 0700); err != nil && !os.IsExist(err) {
return nil, err
}
// Docker makes some assumptions about the "absoluteness" of root
// ... so let's make sure it has no symlinks
if p, err := filepath.Abs(root); err != nil {
log.Fatalf("Unable to get absolute root (%s): %s", root, err)
} else {
root = p
}
if p, err := filepath.EvalSymlinks(root); err != nil {
log.Fatalf("Unable to canonicalize root (%s): %s", root, err)
} else {
root = p
}
eng := &Engine{
root: root,
handlers: make(map[string]Handler),
id: utils.RandomString(),
Stdout: os.Stdout,
Stderr: os.Stderr,
Stdin: os.Stdin,
}
// Copy existing global handlers
for k, v := range globalHandlers {
eng.handlers[k] = v
}
return eng, nil
}
func (eng *Engine) String() string {
return fmt.Sprintf("%s|%s", eng.Root(), eng.id[:8])
}
// Job creates a new job which can later be executed.
// This function mimics `Command` from the standard os/exec package.
func (eng *Engine) Job(name string, args ...string) *Job {
job := &Job{
Eng: eng,
Name: name,
Args: args,
Stdin: NewInput(),
Stdout: NewOutput(),
Stderr: NewOutput(),
env: &Env{},
}
job.Stderr.Add(utils.NopWriteCloser(eng.Stderr))
handler, exists := eng.handlers[name]
if exists {
job.handler = handler
}
return job
}
func (eng *Engine) Logf(format string, args ...interface{}) (n int, err error) {
prefixedFormat := fmt.Sprintf("[%s] %s\n", eng, strings.TrimRight(format, "\n"))
return fmt.Fprintf(eng.Stderr, prefixedFormat, args...)
}

View File

@@ -0,0 +1,111 @@
// Copyright 2014 Docker authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the DOCKER-LICENSE file.
package engine
import (
"io/ioutil"
"os"
"path"
"path/filepath"
"testing"
)
func TestRegister(t *testing.T) {
if err := Register("dummy1", nil); err != nil {
t.Fatal(err)
}
if err := Register("dummy1", nil); err == nil {
t.Fatalf("Expecting error, got none")
}
eng := newTestEngine(t)
//Should fail because global handlers are copied
//at the engine creation
if err := eng.Register("dummy1", nil); err == nil {
t.Fatalf("Expecting error, got none")
}
if err := eng.Register("dummy2", nil); err != nil {
t.Fatal(err)
}
if err := eng.Register("dummy2", nil); err == nil {
t.Fatalf("Expecting error, got none")
}
}
func TestJob(t *testing.T) {
eng := newTestEngine(t)
job1 := eng.Job("dummy1", "--level=awesome")
if job1.handler != nil {
t.Fatalf("job1.handler should be empty")
}
h := func(j *Job) Status {
j.Printf("%s\n", j.Name)
return 42
}
eng.Register("dummy2", h)
job2 := eng.Job("dummy2", "--level=awesome")
if job2.handler == nil {
t.Fatalf("job2.handler shouldn't be nil")
}
if job2.handler(job2) != 42 {
t.Fatalf("handler dummy2 was not found in job2")
}
}
func TestEngineRoot(t *testing.T) {
tmp, err := ioutil.TempDir("", "docker-test-TestEngineCreateDir")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmp)
dir := path.Join(tmp, "dir")
eng, err := New(dir)
if err != nil {
t.Fatal(err)
}
if st, err := os.Stat(dir); err != nil {
t.Fatal(err)
} else if !st.IsDir() {
t.Fatalf("engine.New() created something other than a directory at %s", dir)
}
r := eng.Root()
r, _ = filepath.EvalSymlinks(r)
dir, _ = filepath.EvalSymlinks(dir)
if r != dir {
t.Fatalf("Expected: %v\nReceived: %v", dir, r)
}
}
func TestEngineString(t *testing.T) {
eng1 := newTestEngine(t)
defer os.RemoveAll(eng1.Root())
eng2 := newTestEngine(t)
defer os.RemoveAll(eng2.Root())
s1 := eng1.String()
s2 := eng2.String()
if eng1 == eng2 {
t.Fatalf("Different engines should have different names (%v == %v)", s1, s2)
}
}
func TestEngineLogf(t *testing.T) {
eng := newTestEngine(t)
defer os.RemoveAll(eng.Root())
input := "Test log line"
if n, err := eng.Logf("%s\n", input); err != nil {
t.Fatal(err)
} else if n < len(input) {
t.Fatalf("Test: Logf() should print at least as much as the input\ninput=%d\nprinted=%d", len(input), n)
}
}

View File

@@ -0,0 +1,238 @@
// Copyright 2014 Docker authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the DOCKER-LICENSE file.
package engine
import (
"bytes"
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
)
type Env []string
func (env *Env) Get(key string) (value string) {
// FIXME: use Map()
for _, kv := range *env {
if strings.Index(kv, "=") == -1 {
continue
}
parts := strings.SplitN(kv, "=", 2)
if parts[0] != key {
continue
}
if len(parts) < 2 {
value = ""
} else {
value = parts[1]
}
}
return
}
func (env *Env) Exists(key string) bool {
_, exists := env.Map()[key]
return exists
}
func (env *Env) GetBool(key string) (value bool) {
s := strings.ToLower(strings.Trim(env.Get(key), " \t"))
if s == "" || s == "0" || s == "no" || s == "false" || s == "none" {
return false
}
return true
}
func (env *Env) SetBool(key string, value bool) {
if value {
env.Set(key, "1")
} else {
env.Set(key, "0")
}
}
func (env *Env) GetInt(key string) int {
return int(env.GetInt64(key))
}
func (env *Env) GetInt64(key string) int64 {
s := strings.Trim(env.Get(key), " \t")
val, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return -1
}
return val
}
func (env *Env) SetInt(key string, value int) {
env.Set(key, fmt.Sprintf("%d", value))
}
func (env *Env) SetInt64(key string, value int64) {
env.Set(key, fmt.Sprintf("%d", value))
}
// Returns nil if key not found
func (env *Env) GetList(key string) []string {
sval := env.Get(key)
if sval == "" {
return nil
}
l := make([]string, 0, 1)
if err := json.Unmarshal([]byte(sval), &l); err != nil {
l = append(l, sval)
}
return l
}
func (env *Env) GetJson(key string, iface interface{}) error {
sval := env.Get(key)
if sval == "" {
return nil
}
return json.Unmarshal([]byte(sval), iface)
}
func (env *Env) SetJson(key string, value interface{}) error {
sval, err := json.Marshal(value)
if err != nil {
return err
}
env.Set(key, string(sval))
return nil
}
func (env *Env) SetList(key string, value []string) error {
return env.SetJson(key, value)
}
func (env *Env) Set(key, value string) {
*env = append(*env, key+"="+value)
}
func NewDecoder(src io.Reader) *Decoder {
return &Decoder{
json.NewDecoder(src),
}
}
type Decoder struct {
*json.Decoder
}
func (decoder *Decoder) Decode() (*Env, error) {
m := make(map[string]interface{})
if err := decoder.Decoder.Decode(&m); err != nil {
return nil, err
}
env := &Env{}
for key, value := range m {
env.SetAuto(key, value)
}
return env, nil
}
// DecodeEnv decodes `src` as a json dictionary, and adds
// each decoded key-value pair to the environment.
//
// If `src` cannot be decoded as a json dictionary, an error
// is returned.
func (env *Env) Decode(src io.Reader) error {
m := make(map[string]interface{})
if err := json.NewDecoder(src).Decode(&m); err != nil {
return err
}
for k, v := range m {
env.SetAuto(k, v)
}
return nil
}
func (env *Env) SetAuto(k string, v interface{}) {
// FIXME: we fix-convert float values to int, because
// encoding/json decodes integers to float64, but cannot encode them back.
// (See http://golang.org/src/pkg/encoding/json/decode.go#L46)
if fval, ok := v.(float64); ok {
env.SetInt64(k, int64(fval))
} else if sval, ok := v.(string); ok {
env.Set(k, sval)
} else if val, err := json.Marshal(v); err == nil {
env.Set(k, string(val))
} else {
env.Set(k, fmt.Sprintf("%v", v))
}
}
func (env *Env) Encode(dst io.Writer) error {
m := make(map[string]interface{})
for k, v := range env.Map() {
var val interface{}
if err := json.Unmarshal([]byte(v), &val); err == nil {
// FIXME: we fix-convert float values to int, because
// encoding/json decodes integers to float64, but cannot encode them back.
// (See http://golang.org/src/pkg/encoding/json/decode.go#L46)
if fval, isFloat := val.(float64); isFloat {
val = int(fval)
}
m[k] = val
} else {
m[k] = v
}
}
if err := json.NewEncoder(dst).Encode(&m); err != nil {
return err
}
return nil
}
func (env *Env) WriteTo(dst io.Writer) (n int64, err error) {
// FIXME: return the number of bytes written to respect io.WriterTo
return 0, env.Encode(dst)
}
func (env *Env) Export(dst interface{}) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("ExportEnv %s", err)
}
}()
var buf bytes.Buffer
// step 1: encode/marshal the env to an intermediary json representation
if err := env.Encode(&buf); err != nil {
return err
}
// step 2: decode/unmarshal the intermediary json into the destination object
if err := json.NewDecoder(&buf).Decode(dst); err != nil {
return err
}
return nil
}
func (env *Env) Import(src interface{}) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("ImportEnv: %s", err)
}
}()
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(src); err != nil {
return err
}
if err := env.Decode(&buf); err != nil {
return err
}
return nil
}
func (env *Env) Map() map[string]string {
m := make(map[string]string)
for _, kv := range *env {
parts := strings.SplitN(kv, "=", 2)
m[parts[0]] = parts[1]
}
return m
}

View File

@@ -0,0 +1,127 @@
// Copyright 2014 Docker authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the DOCKER-LICENSE file.
package engine
import (
"testing"
)
func TestNewJob(t *testing.T) {
job := mkJob(t, "dummy", "--level=awesome")
if job.Name != "dummy" {
t.Fatalf("Wrong job name: %s", job.Name)
}
if len(job.Args) != 1 {
t.Fatalf("Wrong number of job arguments: %d", len(job.Args))
}
if job.Args[0] != "--level=awesome" {
t.Fatalf("Wrong job arguments: %s", job.Args[0])
}
}
func TestSetenv(t *testing.T) {
job := mkJob(t, "dummy")
job.Setenv("foo", "bar")
if val := job.Getenv("foo"); val != "bar" {
t.Fatalf("Getenv returns incorrect value: %s", val)
}
job.Setenv("bar", "")
if val := job.Getenv("bar"); val != "" {
t.Fatalf("Getenv returns incorrect value: %s", val)
}
if val := job.Getenv("nonexistent"); val != "" {
t.Fatalf("Getenv returns incorrect value: %s", val)
}
}
func TestSetenvBool(t *testing.T) {
job := mkJob(t, "dummy")
job.SetenvBool("foo", true)
if val := job.GetenvBool("foo"); !val {
t.Fatalf("GetenvBool returns incorrect value: %t", val)
}
job.SetenvBool("bar", false)
if val := job.GetenvBool("bar"); val {
t.Fatalf("GetenvBool returns incorrect value: %t", val)
}
if val := job.GetenvBool("nonexistent"); val {
t.Fatalf("GetenvBool returns incorrect value: %t", val)
}
}
func TestSetenvInt(t *testing.T) {
job := mkJob(t, "dummy")
job.SetenvInt("foo", -42)
if val := job.GetenvInt("foo"); val != -42 {
t.Fatalf("GetenvInt returns incorrect value: %d", val)
}
job.SetenvInt("bar", 42)
if val := job.GetenvInt("bar"); val != 42 {
t.Fatalf("GetenvInt returns incorrect value: %d", val)
}
if val := job.GetenvInt("nonexistent"); val != -1 {
t.Fatalf("GetenvInt returns incorrect value: %d", val)
}
}
func TestSetenvList(t *testing.T) {
job := mkJob(t, "dummy")
job.SetenvList("foo", []string{"bar"})
if val := job.GetenvList("foo"); len(val) != 1 || val[0] != "bar" {
t.Fatalf("GetenvList returns incorrect value: %v", val)
}
job.SetenvList("bar", nil)
if val := job.GetenvList("bar"); val != nil {
t.Fatalf("GetenvList returns incorrect value: %v", val)
}
if val := job.GetenvList("nonexistent"); val != nil {
t.Fatalf("GetenvList returns incorrect value: %v", val)
}
}
func TestImportEnv(t *testing.T) {
type dummy struct {
DummyInt int
DummyStringArray []string
}
job := mkJob(t, "dummy")
if err := job.ImportEnv(&dummy{42, []string{"foo", "bar"}}); err != nil {
t.Fatal(err)
}
dmy := dummy{}
if err := job.ExportEnv(&dmy); err != nil {
t.Fatal(err)
}
if dmy.DummyInt != 42 {
t.Fatalf("Expected 42, got %d", dmy.DummyInt)
}
if len(dmy.DummyStringArray) != 2 || dmy.DummyStringArray[0] != "foo" || dmy.DummyStringArray[1] != "bar" {
t.Fatalf("Expected {foo, bar}, got %v", dmy.DummyStringArray)
}
}
func TestEnviron(t *testing.T) {
job := mkJob(t, "dummy")
job.Setenv("foo", "bar")
val, exists := job.Environ()["foo"]
if !exists {
t.Fatalf("foo not found in the environ")
}
if val != "bar" {
t.Fatalf("bar not found in the environ")
}
}

View File

@@ -0,0 +1,25 @@
// Copyright 2014 Docker authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the DOCKER-LICENSE file.
package engine
type Hack map[string]interface{}
func (eng *Engine) Hack_GetGlobalVar(key string) interface{} {
if eng.hack == nil {
return nil
}
val, exists := eng.hack[key]
if !exists {
return nil
}
return val
}
func (eng *Engine) Hack_SetGlobalVar(key string, val interface{}) {
if eng.hack == nil {
eng.hack = make(Hack)
}
eng.hack[key] = val
}

View File

@@ -0,0 +1,28 @@
// Copyright 2014 Docker authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the DOCKER-LICENSE file.
package engine
import (
"io/ioutil"
"testing"
)
var globalTestID string
func newTestEngine(t *testing.T) *Engine {
tmp, err := ioutil.TempDir("", "asd")
if err != nil {
t.Fatal(err)
}
eng, err := New(tmp)
if err != nil {
t.Fatal(err)
}
return eng
}
func mkJob(t *testing.T, name string, args ...string) *Job {
return newTestEngine(t).Job(name, args...)
}

View File

@@ -0,0 +1,44 @@
// Copyright 2014 Docker authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the DOCKER-LICENSE file.
package engine
import (
"net/http"
"path"
)
// ServeHTTP executes a job as specified by the http request `r`, and sends the
// result as an http response.
// This method allows an Engine instance to be passed as a standard http.Handler interface.
//
// Note that the protocol used in this methid is a convenience wrapper and is not the canonical
// implementation of remote job execution. This is because HTTP/1 does not handle stream multiplexing,
// and so cannot differentiate stdout from stderr. Additionally, headers cannot be added to a response
// once data has been written to the body, which makes it inconvenient to return metadata such
// as the exit status.
//
func (eng *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
jobName := path.Base(r.URL.Path)
jobArgs, exists := r.URL.Query()["a"]
if !exists {
jobArgs = []string{}
}
w.Header().Set("Job-Name", jobName)
for _, arg := range jobArgs {
w.Header().Add("Job-Args", arg)
}
job := eng.Job(jobName, jobArgs...)
job.Stdout.Add(w)
job.Stderr.Add(w)
// FIXME: distinguish job status from engine error in Run()
// The former should be passed as a special header, the former
// should cause a 500 status
w.WriteHeader(http.StatusOK)
// The exit status cannot be sent reliably with HTTP1, because headers
// can only be sent before the body.
// (we could possibly use http footers via chunked encoding, but I couldn't find
// how to use them in net/http)
job.Run()
}

View File

@@ -0,0 +1,197 @@
// Copyright 2014 Docker authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the DOCKER-LICENSE file.
package engine
import (
"fmt"
"io"
"strings"
"time"
)
// A job is the fundamental unit of work in the docker engine.
// Everything docker can do should eventually be exposed as a job.
// For example: execute a process in a container, create a new container,
// download an archive from the internet, serve the http api, etc.
//
// The job API is designed after unix processes: a job has a name, arguments,
// environment variables, standard streams for input, output and error, and
// an exit status which can indicate success (0) or error (anything else).
//
// One slight variation is that jobs report their status as a string. The
// string "0" indicates success, and any other strings indicates an error.
// This allows for richer error reporting.
//
type Job struct {
Eng *Engine
Name string
Args []string
env *Env
Stdout *Output
Stderr *Output
Stdin *Input
handler Handler
status Status
end time.Time
onExit []func()
}
type Status int
const (
StatusOK Status = 0
StatusErr Status = 1
StatusNotFound Status = 127
)
// Run executes the job and blocks until the job completes.
// If the job returns a failure status, an error is returned
// which includes the status.
func (job *Job) Run() error {
// FIXME: make this thread-safe
// FIXME: implement wait
if !job.end.IsZero() {
return fmt.Errorf("%s: job has already completed", job.Name)
}
// Log beginning and end of the job
job.Eng.Logf("+job %s", job.CallString())
defer func() {
job.Eng.Logf("-job %s%s", job.CallString(), job.StatusString())
}()
var errorMessage string
job.Stderr.AddString(&errorMessage)
if job.handler == nil {
job.Errorf("%s: command not found", job.Name)
job.status = 127
} else {
job.status = job.handler(job)
job.end = time.Now()
}
// Wait for all background tasks to complete
if err := job.Stdout.Close(); err != nil {
return err
}
if err := job.Stderr.Close(); err != nil {
return err
}
if job.status != 0 {
return fmt.Errorf("%s: %s", job.Name, errorMessage)
}
return nil
}
func (job *Job) CallString() string {
return fmt.Sprintf("%s(%s)", job.Name, strings.Join(job.Args, ", "))
}
func (job *Job) StatusString() string {
// If the job hasn't completed, status string is empty
if job.end.IsZero() {
return ""
}
var okerr string
if job.status == StatusOK {
okerr = "OK"
} else {
okerr = "ERR"
}
return fmt.Sprintf(" = %s (%d)", okerr, job.status)
}
// String returns a human-readable description of `job`
func (job *Job) String() string {
return fmt.Sprintf("%s.%s%s", job.Eng, job.CallString(), job.StatusString())
}
func (job *Job) Getenv(key string) (value string) {
return job.env.Get(key)
}
func (job *Job) GetenvBool(key string) (value bool) {
return job.env.GetBool(key)
}
func (job *Job) SetenvBool(key string, value bool) {
job.env.SetBool(key, value)
}
func (job *Job) GetenvInt64(key string) int64 {
return job.env.GetInt64(key)
}
func (job *Job) GetenvInt(key string) int {
return job.env.GetInt(key)
}
func (job *Job) SetenvInt64(key string, value int64) {
job.env.SetInt64(key, value)
}
func (job *Job) SetenvInt(key string, value int) {
job.env.SetInt(key, value)
}
// Returns nil if key not found
func (job *Job) GetenvList(key string) []string {
return job.env.GetList(key)
}
func (job *Job) GetenvJson(key string, iface interface{}) error {
return job.env.GetJson(key, iface)
}
func (job *Job) SetenvJson(key string, value interface{}) error {
return job.env.SetJson(key, value)
}
func (job *Job) SetenvList(key string, value []string) error {
return job.env.SetJson(key, value)
}
func (job *Job) Setenv(key, value string) {
job.env.Set(key, value)
}
// DecodeEnv decodes `src` as a json dictionary, and adds
// each decoded key-value pair to the environment.
//
// If `src` cannot be decoded as a json dictionary, an error
// is returned.
func (job *Job) DecodeEnv(src io.Reader) error {
return job.env.Decode(src)
}
func (job *Job) EncodeEnv(dst io.Writer) error {
return job.env.Encode(dst)
}
func (job *Job) ExportEnv(dst interface{}) (err error) {
return job.env.Export(dst)
}
func (job *Job) ImportEnv(src interface{}) (err error) {
return job.env.Import(src)
}
func (job *Job) Environ() map[string]string {
return job.env.Map()
}
func (job *Job) Logf(format string, args ...interface{}) (n int, err error) {
prefixedFormat := fmt.Sprintf("[%s] %s\n", job, strings.TrimRight(format, "\n"))
return fmt.Fprintf(job.Stderr, prefixedFormat, args...)
}
func (job *Job) Printf(format string, args ...interface{}) (n int, err error) {
return fmt.Fprintf(job.Stdout, format, args...)
}
func (job *Job) Errorf(format string, args ...interface{}) (n int, err error) {
return fmt.Fprintf(job.Stderr, format, args...)
}
func (job *Job) Error(err error) (int, error) {
return fmt.Fprintf(job.Stderr, "%s", err)
}

View File

@@ -0,0 +1,84 @@
// Copyright 2014 Docker authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the DOCKER-LICENSE file.
package engine
import (
"os"
"testing"
)
func TestJobStatusOK(t *testing.T) {
eng := newTestEngine(t)
defer os.RemoveAll(eng.Root())
eng.Register("return_ok", func(job *Job) Status { return StatusOK })
err := eng.Job("return_ok").Run()
if err != nil {
t.Fatalf("Expected: err=%v\nReceived: err=%v", nil, err)
}
}
func TestJobStatusErr(t *testing.T) {
eng := newTestEngine(t)
defer os.RemoveAll(eng.Root())
eng.Register("return_err", func(job *Job) Status { return StatusErr })
err := eng.Job("return_err").Run()
if err == nil {
t.Fatalf("When a job returns StatusErr, Run() should return an error")
}
}
func TestJobStatusNotFound(t *testing.T) {
eng := newTestEngine(t)
defer os.RemoveAll(eng.Root())
eng.Register("return_not_found", func(job *Job) Status { return StatusNotFound })
err := eng.Job("return_not_found").Run()
if err == nil {
t.Fatalf("When a job returns StatusNotFound, Run() should return an error")
}
}
func TestJobStdoutString(t *testing.T) {
eng := newTestEngine(t)
defer os.RemoveAll(eng.Root())
// FIXME: test multiple combinations of output and status
eng.Register("say_something_in_stdout", func(job *Job) Status {
job.Printf("Hello world\n")
return StatusOK
})
job := eng.Job("say_something_in_stdout")
var output string
if err := job.Stdout.AddString(&output); err != nil {
t.Fatal(err)
}
if err := job.Run(); err != nil {
t.Fatal(err)
}
if expectedOutput := "Hello world"; output != expectedOutput {
t.Fatalf("Stdout last line:\nExpected: %v\nReceived: %v", expectedOutput, output)
}
}
func TestJobStderrString(t *testing.T) {
eng := newTestEngine(t)
defer os.RemoveAll(eng.Root())
// FIXME: test multiple combinations of output and status
eng.Register("say_something_in_stderr", func(job *Job) Status {
job.Errorf("Warning, something might happen\nHere it comes!\nOh no...\nSomething happened\n")
return StatusOK
})
job := eng.Job("say_something_in_stderr")
var output string
if err := job.Stderr.AddString(&output); err != nil {
t.Fatal(err)
}
if err := job.Run(); err != nil {
t.Fatal(err)
}
if expectedOutput := "Something happened"; output != expectedOutput {
t.Fatalf("Stderr last line:\nExpected: %v\nReceived: %v", expectedOutput, output)
}
}

View File

@@ -0,0 +1,196 @@
// Copyright 2014 Docker authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the DOCKER-LICENSE file.
package engine
import (
"bufio"
"container/ring"
"fmt"
"io"
"sync"
)
type Output struct {
sync.Mutex
dests []io.Writer
tasks sync.WaitGroup
}
// NewOutput returns a new Output object with no destinations attached.
// Writing to an empty Output will cause the written data to be discarded.
func NewOutput() *Output {
return &Output{}
}
// Add attaches a new destination to the Output. Any data subsequently written
// to the output will be written to the new destination in addition to all the others.
// This method is thread-safe.
// FIXME: Add cannot fail
func (o *Output) Add(dst io.Writer) error {
o.Mutex.Lock()
defer o.Mutex.Unlock()
o.dests = append(o.dests, dst)
return nil
}
// AddPipe creates an in-memory pipe with io.Pipe(), adds its writing end as a destination,
// and returns its reading end for consumption by the caller.
// This is a rough equivalent similar to Cmd.StdoutPipe() in the standard os/exec package.
// This method is thread-safe.
func (o *Output) AddPipe() (io.Reader, error) {
r, w := io.Pipe()
o.Add(w)
return r, nil
}
// AddTail starts a new goroutine which will read all subsequent data written to the output,
// line by line, and append the last `n` lines to `dst`.
func (o *Output) AddTail(dst *[]string, n int) error {
src, err := o.AddPipe()
if err != nil {
return err
}
o.tasks.Add(1)
go func() {
defer o.tasks.Done()
Tail(src, n, dst)
}()
return nil
}
// AddString starts a new goroutine which will read all subsequent data written to the output,
// line by line, and store the last line into `dst`.
func (o *Output) AddString(dst *string) error {
src, err := o.AddPipe()
if err != nil {
return err
}
o.tasks.Add(1)
go func() {
defer o.tasks.Done()
lines := make([]string, 0, 1)
Tail(src, 1, &lines)
if len(lines) == 0 {
*dst = ""
} else {
*dst = lines[0]
}
}()
return nil
}
// Write writes the same data to all registered destinations.
// This method is thread-safe.
func (o *Output) Write(p []byte) (n int, err error) {
o.Mutex.Lock()
defer o.Mutex.Unlock()
var firstErr error
for _, dst := range o.dests {
_, err := dst.Write(p)
if err != nil && firstErr == nil {
firstErr = err
}
}
return len(p), firstErr
}
// Close unregisters all destinations and waits for all background
// AddTail and AddString tasks to complete.
// The Close method of each destination is called if it exists.
func (o *Output) Close() error {
o.Mutex.Lock()
defer o.Mutex.Unlock()
var firstErr error
for _, dst := range o.dests {
if closer, ok := dst.(io.WriteCloser); ok {
err := closer.Close()
if err != nil && firstErr == nil {
firstErr = err
}
}
}
o.tasks.Wait()
return firstErr
}
type Input struct {
src io.Reader
sync.Mutex
}
// NewInput returns a new Input object with no source attached.
// Reading to an empty Input will return io.EOF.
func NewInput() *Input {
return &Input{}
}
// Read reads from the input in a thread-safe way.
func (i *Input) Read(p []byte) (n int, err error) {
i.Mutex.Lock()
defer i.Mutex.Unlock()
if i.src == nil {
return 0, io.EOF
}
return i.src.Read(p)
}
// Add attaches a new source to the input.
// Add can only be called once per input. Subsequent calls will
// return an error.
func (i *Input) Add(src io.Reader) error {
i.Mutex.Lock()
defer i.Mutex.Unlock()
if i.src != nil {
return fmt.Errorf("Maximum number of sources reached: 1")
}
i.src = src
return nil
}
// Tail reads from `src` line per line, and returns the last `n` lines as an array.
// A ring buffer is used to only store `n` lines at any time.
func Tail(src io.Reader, n int, dst *[]string) {
scanner := bufio.NewScanner(src)
r := ring.New(n)
for scanner.Scan() {
if n == 0 {
continue
}
r.Value = scanner.Text()
r = r.Next()
}
r.Do(func(v interface{}) {
if v == nil {
return
}
*dst = append(*dst, v.(string))
})
}
// AddEnv starts a new goroutine which will decode all subsequent data
// as a stream of json-encoded objects, and point `dst` to the last
// decoded object.
// The result `env` can be queried using the type-neutral Env interface.
// It is not safe to query `env` until the Output is closed.
func (o *Output) AddEnv() (dst *Env, err error) {
src, err := o.AddPipe()
if err != nil {
return nil, err
}
dst = &Env{}
o.tasks.Add(1)
go func() {
defer o.tasks.Done()
decoder := NewDecoder(src)
for {
env, err := decoder.Decode()
if err != nil {
return
}
*dst = *env
}
}()
return dst, nil
}

View File

@@ -0,0 +1,298 @@
// Copyright 2014 Docker authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the DOCKER-LICENSE file.
package engine
import (
"bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
"strings"
"testing"
)
func TestOutputAddString(t *testing.T) {
var testInputs = [][2]string{
{
"hello, world!",
"hello, world!",
},
{
"One\nTwo\nThree",
"Three",
},
{
"",
"",
},
{
"A line\nThen another nl-terminated line\n",
"Then another nl-terminated line",
},
{
"A line followed by an empty line\n\n",
"",
},
}
for _, testData := range testInputs {
input := testData[0]
expectedOutput := testData[1]
o := NewOutput()
var output string
if err := o.AddString(&output); err != nil {
t.Error(err)
}
if n, err := o.Write([]byte(input)); err != nil {
t.Error(err)
} else if n != len(input) {
t.Errorf("Expected %d, got %d", len(input), n)
}
o.Close()
if output != expectedOutput {
t.Errorf("Last line is not stored as return string.\nInput: '%s'\nExpected: '%s'\nGot: '%s'", input, expectedOutput, output)
}
}
}
type sentinelWriteCloser struct {
calledWrite bool
calledClose bool
}
func (w *sentinelWriteCloser) Write(p []byte) (int, error) {
w.calledWrite = true
return len(p), nil
}
func (w *sentinelWriteCloser) Close() error {
w.calledClose = true
return nil
}
func TestOutputAddEnv(t *testing.T) {
input := "{\"foo\": \"bar\", \"answer_to_life_the_universe_and_everything\": 42}"
o := NewOutput()
result, err := o.AddEnv()
if err != nil {
t.Fatal(err)
}
o.Write([]byte(input))
o.Close()
if v := result.Get("foo"); v != "bar" {
t.Errorf("Expected %v, got %v", "bar", v)
}
if v := result.GetInt("answer_to_life_the_universe_and_everything"); v != 42 {
t.Errorf("Expected %v, got %v", 42, v)
}
if v := result.Get("this-value-doesnt-exist"); v != "" {
t.Errorf("Expected %v, got %v", "", v)
}
}
func TestOutputAddClose(t *testing.T) {
o := NewOutput()
var s sentinelWriteCloser
if err := o.Add(&s); err != nil {
t.Fatal(err)
}
if err := o.Close(); err != nil {
t.Fatal(err)
}
// Write data after the output is closed.
// Write should succeed, but no destination should receive it.
if _, err := o.Write([]byte("foo bar")); err != nil {
t.Fatal(err)
}
if !s.calledClose {
t.Fatal("Output.Close() didn't close the destination")
}
}
func TestOutputAddPipe(t *testing.T) {
var testInputs = []string{
"hello, world!",
"One\nTwo\nThree",
"",
"A line\nThen another nl-terminated line\n",
"A line followed by an empty line\n\n",
}
for _, input := range testInputs {
expectedOutput := input
o := NewOutput()
r, err := o.AddPipe()
if err != nil {
t.Fatal(err)
}
go func(o *Output) {
if n, err := o.Write([]byte(input)); err != nil {
t.Error(err)
} else if n != len(input) {
t.Errorf("Expected %d, got %d", len(input), n)
}
if err := o.Close(); err != nil {
t.Error(err)
}
}(o)
output, err := ioutil.ReadAll(r)
if err != nil {
t.Fatal(err)
}
if string(output) != expectedOutput {
t.Errorf("Last line is not stored as return string.\nExpected: '%s'\nGot: '%s'", expectedOutput, output)
}
}
}
func TestTail(t *testing.T) {
var tests = make(map[string][][]string)
tests["hello, world!"] = [][]string{
{},
{"hello, world!"},
{"hello, world!"},
{"hello, world!"},
}
tests["One\nTwo\nThree"] = [][]string{
{},
{"Three"},
{"Two", "Three"},
{"One", "Two", "Three"},
}
for input, outputs := range tests {
for n, expectedOutput := range outputs {
var output []string
Tail(strings.NewReader(input), n, &output)
if fmt.Sprintf("%v", output) != fmt.Sprintf("%v", expectedOutput) {
t.Errorf("Tail n=%d returned wrong result.\nExpected: '%s'\nGot : '%s'", n, expectedOutput, output)
}
}
}
}
func TestOutputAddTail(t *testing.T) {
var tests = make(map[string][][]string)
tests["hello, world!"] = [][]string{
{},
{"hello, world!"},
{"hello, world!"},
{"hello, world!"},
}
tests["One\nTwo\nThree"] = [][]string{
{},
{"Three"},
{"Two", "Three"},
{"One", "Two", "Three"},
}
for input, outputs := range tests {
for n, expectedOutput := range outputs {
o := NewOutput()
var output []string
if err := o.AddTail(&output, n); err != nil {
t.Error(err)
}
if n, err := o.Write([]byte(input)); err != nil {
t.Error(err)
} else if n != len(input) {
t.Errorf("Expected %d, got %d", len(input), n)
}
o.Close()
if fmt.Sprintf("%v", output) != fmt.Sprintf("%v", expectedOutput) {
t.Errorf("Tail(%d) returned wrong result.\nExpected: %v\nGot: %v", n, expectedOutput, output)
}
}
}
}
func lastLine(txt string) string {
scanner := bufio.NewScanner(strings.NewReader(txt))
var lastLine string
for scanner.Scan() {
lastLine = scanner.Text()
}
return lastLine
}
func TestOutputAdd(t *testing.T) {
o := NewOutput()
b := &bytes.Buffer{}
o.Add(b)
input := "hello, world!"
if n, err := o.Write([]byte(input)); err != nil {
t.Fatal(err)
} else if n != len(input) {
t.Fatalf("Expected %d, got %d", len(input), n)
}
if output := b.String(); output != input {
t.Fatalf("Received wrong data from Add.\nExpected: '%s'\nGot: '%s'", input, output)
}
}
func TestOutputWriteError(t *testing.T) {
o := NewOutput()
buf := &bytes.Buffer{}
o.Add(buf)
r, w := io.Pipe()
input := "Hello there"
expectedErr := fmt.Errorf("This is an error")
r.CloseWithError(expectedErr)
o.Add(w)
n, err := o.Write([]byte(input))
if err != expectedErr {
t.Fatalf("Output.Write() should return the first error encountered, if any")
}
if buf.String() != input {
t.Fatalf("Output.Write() should attempt write on all destinations, even after encountering an error")
}
if n != len(input) {
t.Fatalf("Output.Write() should return the size of the input if it successfully writes to at least one destination")
}
}
func TestInputAddEmpty(t *testing.T) {
i := NewInput()
var b bytes.Buffer
if err := i.Add(&b); err != nil {
t.Fatal(err)
}
data, err := ioutil.ReadAll(i)
if err != nil {
t.Fatal(err)
}
if len(data) > 0 {
t.Fatalf("Read from empty input shoul yield no data")
}
}
func TestInputAddTwo(t *testing.T) {
i := NewInput()
var b1 bytes.Buffer
// First add should succeed
if err := i.Add(&b1); err != nil {
t.Fatal(err)
}
var b2 bytes.Buffer
// Second add should fail
if err := i.Add(&b2); err == nil {
t.Fatalf("Adding a second source should return an error")
}
}
func TestInputAddNotEmpty(t *testing.T) {
i := NewInput()
b := bytes.NewBufferString("hello world\nabc")
expectedResult := b.String()
i.Add(b)
result, err := ioutil.ReadAll(i)
if err != nil {
t.Fatal(err)
}
if string(result) != expectedResult {
t.Fatalf("Expected: %v\nReceived: %v", expectedResult, result)
}
}

View File

@@ -0,0 +1,279 @@
// Copyright 2014 go-dockerclient authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package docker
import (
"encoding/json"
"errors"
"fmt"
"io"
"math"
"net"
"net/http"
"net/http/httputil"
"sync"
"sync/atomic"
"time"
)
// APIEvents represents an event returned by the API.
type APIEvents struct {
Status string
ID string
From string
Time int64
}
type eventMonitoringState struct {
sync.RWMutex
sync.WaitGroup
enabled bool
lastSeen *int64
C chan *APIEvents
errC chan error
listeners []chan<- *APIEvents
}
const (
maxMonitorConnRetries = 5
retryInitialWaitTime = 10.
)
var (
// ErrNoListeners is the error returned when no listeners are available
// to receive an event.
ErrNoListeners = errors.New("no listeners present to receive event")
// ErrListenerAlreadyExists is the error returned when the listerner already
// exists.
ErrListenerAlreadyExists = errors.New("listener already exists for docker events")
)
// AddEventListener adds a new listener to container events in the Docker API.
//
// The parameter is a channel through which events will be sent.
func (c *Client) AddEventListener(listener chan<- *APIEvents) error {
var err error
if !c.eventMonitor.isEnabled() {
err = c.eventMonitor.enableEventMonitoring(c)
if err != nil {
return err
}
}
err = c.eventMonitor.addListener(listener)
if err != nil {
return err
}
return nil
}
// RemoveEventListener removes a listener from the monitor.
func (c *Client) RemoveEventListener(listener chan *APIEvents) error {
err := c.eventMonitor.removeListener(listener)
if err != nil {
return err
}
if len(c.eventMonitor.listeners) == 0 {
err = c.eventMonitor.disableEventMonitoring()
if err != nil {
return err
}
}
return nil
}
func (eventState *eventMonitoringState) addListener(listener chan<- *APIEvents) error {
eventState.Lock()
defer eventState.Unlock()
if listenerExists(listener, &eventState.listeners) {
return ErrListenerAlreadyExists
}
eventState.Add(1)
eventState.listeners = append(eventState.listeners, listener)
return nil
}
func (eventState *eventMonitoringState) removeListener(listener chan<- *APIEvents) error {
eventState.Lock()
defer eventState.Unlock()
if listenerExists(listener, &eventState.listeners) {
var newListeners []chan<- *APIEvents
for _, l := range eventState.listeners {
if l != listener {
newListeners = append(newListeners, l)
}
}
eventState.listeners = newListeners
eventState.Add(-1)
}
return nil
}
func listenerExists(a chan<- *APIEvents, list *[]chan<- *APIEvents) bool {
for _, b := range *list {
if b == a {
return true
}
}
return false
}
func (eventState *eventMonitoringState) enableEventMonitoring(c *Client) error {
eventState.Lock()
defer eventState.Unlock()
if !eventState.enabled {
eventState.enabled = true
var lastSeenDefault = int64(0)
eventState.lastSeen = &lastSeenDefault
eventState.C = make(chan *APIEvents, 100)
eventState.errC = make(chan error, 1)
go eventState.monitorEvents(c)
}
return nil
}
func (eventState *eventMonitoringState) disableEventMonitoring() error {
eventState.Wait()
eventState.Lock()
defer eventState.Unlock()
if eventState.enabled {
eventState.enabled = false
close(eventState.C)
close(eventState.errC)
}
return nil
}
func (eventState *eventMonitoringState) monitorEvents(c *Client) {
var err error
for eventState.noListeners() {
time.Sleep(10 * time.Millisecond)
}
if err = eventState.connectWithRetry(c); err != nil {
eventState.terminate(err)
}
for eventState.isEnabled() {
timeout := time.After(100 * time.Millisecond)
select {
case ev, ok := <-eventState.C:
if !ok {
return
}
go eventState.sendEvent(ev)
go eventState.updateLastSeen(ev)
case err = <-eventState.errC:
if err == ErrNoListeners {
eventState.terminate(nil)
return
} else if err != nil {
defer func() { go eventState.monitorEvents(c) }()
return
}
case <-timeout:
continue
}
}
}
func (eventState *eventMonitoringState) connectWithRetry(c *Client) error {
var retries int
var err error
for err = c.eventHijack(atomic.LoadInt64(eventState.lastSeen), eventState.C, eventState.errC); err != nil && retries < maxMonitorConnRetries; retries++ {
waitTime := int64(retryInitialWaitTime * math.Pow(2, float64(retries)))
time.Sleep(time.Duration(waitTime) * time.Millisecond)
err = c.eventHijack(atomic.LoadInt64(eventState.lastSeen), eventState.C, eventState.errC)
}
return err
}
func (eventState *eventMonitoringState) noListeners() bool {
eventState.RLock()
defer eventState.RUnlock()
return len(eventState.listeners) == 0
}
func (eventState *eventMonitoringState) isEnabled() bool {
eventState.RLock()
defer eventState.RUnlock()
return eventState.enabled
}
func (eventState *eventMonitoringState) sendEvent(event *APIEvents) {
eventState.RLock()
defer eventState.RUnlock()
eventState.Add(1)
defer eventState.Done()
if eventState.isEnabled() {
if eventState.noListeners() {
eventState.errC <- ErrNoListeners
return
}
for _, listener := range eventState.listeners {
listener <- event
}
}
}
func (eventState *eventMonitoringState) updateLastSeen(e *APIEvents) {
eventState.Lock()
defer eventState.Unlock()
if atomic.LoadInt64(eventState.lastSeen) < e.Time {
atomic.StoreInt64(eventState.lastSeen, e.Time)
}
}
func (eventState *eventMonitoringState) terminate(err error) {
eventState.disableEventMonitoring()
}
func (c *Client) eventHijack(startTime int64, eventChan chan *APIEvents, errChan chan error) error {
uri := "/events"
if startTime != 0 {
uri += fmt.Sprintf("?since=%d", startTime)
}
protocol := c.endpointURL.Scheme
address := c.endpointURL.Path
if protocol != "unix" {
protocol = "tcp"
address = c.endpointURL.Host
}
dial, err := net.Dial(protocol, address)
if err != nil {
return err
}
conn := httputil.NewClientConn(dial, nil)
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
return err
}
res, err := conn.Do(req)
if err != nil {
return err
}
go func(res *http.Response, conn *httputil.ClientConn) {
defer conn.Close()
defer res.Body.Close()
decoder := json.NewDecoder(res.Body)
for {
var event APIEvents
if err = decoder.Decode(&event); err != nil {
if err == io.EOF {
break
}
errChan <- err
}
if event.Time == 0 {
continue
}
if !c.eventMonitor.isEnabled() {
return
} else {
c.eventMonitor.C <- &event
}
}
}(res, conn)
return nil
}

View File

@@ -0,0 +1,92 @@
// Copyright 2014 go-dockerclient authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package docker
import (
"bufio"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestEventListeners(t *testing.T) {
response := `{"status":"create","id":"dfdf82bd3881","from":"base:latest","time":1374067924}
{"status":"start","id":"dfdf82bd3881","from":"base:latest","time":1374067924}
{"status":"stop","id":"dfdf82bd3881","from":"base:latest","time":1374067966}
{"status":"destroy","id":"dfdf82bd3881","from":"base:latest","time":1374067970}
`
var req http.Request
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rsc := bufio.NewScanner(strings.NewReader(response))
for rsc.Scan() {
w.Write([]byte(rsc.Text()))
w.(http.Flusher).Flush()
time.Sleep(10 * time.Millisecond)
}
req = *r
}))
defer server.Close()
client, err := NewClient(server.URL)
if err != nil {
t.Errorf("Failed to create client: %s", err)
}
listener := make(chan *APIEvents, 10)
defer func() { time.Sleep(10 * time.Millisecond); client.RemoveEventListener(listener) }()
err = client.AddEventListener(listener)
if err != nil {
t.Errorf("Failed to add event listener: %s", err)
}
timeout := time.After(1 * time.Second)
var count int
for {
select {
case msg := <-listener:
t.Logf("Recieved: %s", *msg)
count++
err = checkEvent(count, msg)
if err != nil {
t.Fatalf("Check event failed: %s", err)
}
if count == 4 {
return
}
case <-timeout:
t.Fatal("TestAddEventListener timed out waiting on events")
}
}
}
func checkEvent(index int, event *APIEvents) error {
if event.ID != "dfdf82bd3881" {
return fmt.Errorf("event ID did not match. Expected dfdf82bd3881 got %s", event.ID)
}
if event.From != "base:latest" {
return fmt.Errorf("event from did not match. Expected base:latest got %s", event.From)
}
var status string
switch index {
case 1:
status = "create"
case 2:
status = "start"
case 3:
status = "stop"
case 4:
status = "destroy"
}
if event.Status != status {
return fmt.Errorf("event status did not match. Expected %s got %s", status, event.Status)
}
return nil
}

View File

@@ -0,0 +1,133 @@
// Copyright 2014 go-dockerclient authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package docker_test
import (
"archive/tar"
"bytes"
"io"
"log"
"time"
"github.com/fsouza/go-dockerclient"
)
func ExampleClient_AttachToContainer() {
client, err := docker.NewClient("http://localhost:4243")
if err != nil {
log.Fatal(err)
}
// Reading logs from container a84849 and sending them to buf.
var buf bytes.Buffer
err = client.AttachToContainer(docker.AttachToContainerOptions{
Container: "a84849",
OutputStream: &buf,
Logs: true,
Stdout: true,
Stderr: true,
})
if err != nil {
log.Fatal(err)
}
log.Println(buf.String())
// Attaching to stdout and streaming.
buf.Reset()
err = client.AttachToContainer(docker.AttachToContainerOptions{
Container: "a84849",
OutputStream: &buf,
Stdout: true,
Stream: true,
})
if err != nil {
log.Fatal(err)
}
log.Println(buf.String())
}
func ExampleClient_CopyFromContainer() {
client, err := docker.NewClient("http://localhost:4243")
if err != nil {
log.Fatal(err)
}
cid := "a84849"
// Copy resulting file
var buf bytes.Buffer
filename := "/tmp/output.txt"
err = client.CopyFromContainer(docker.CopyFromContainerOptions{
Container: cid,
Resource: filename,
OutputStream: &buf,
})
if err != nil {
log.Fatalf("Error while copying from %s: %s\n", cid, err)
}
content := new(bytes.Buffer)
r := bytes.NewReader(buf.Bytes())
tr := tar.NewReader(r)
tr.Next()
if err != nil && err != io.EOF {
log.Fatal(err)
}
if _, err := io.Copy(content, tr); err != nil {
log.Fatal(err)
}
log.Println(buf.String())
}
func ExampleClient_BuildImage() {
client, err := docker.NewClient("http://localhost:4243")
if err != nil {
log.Fatal(err)
}
t := time.Now()
inputbuf, outputbuf := bytes.NewBuffer(nil), bytes.NewBuffer(nil)
tr := tar.NewWriter(inputbuf)
tr.WriteHeader(&tar.Header{Name: "Dockerfile", Size: 10, ModTime: t, AccessTime: t, ChangeTime: t})
tr.Write([]byte("FROM base\n"))
tr.Close()
opts := docker.BuildImageOptions{
Name: "test",
InputStream: inputbuf,
OutputStream: outputbuf,
}
if err := client.BuildImage(opts); err != nil {
log.Fatal(err)
}
}
func ExampleClient_ListenEvents() {
client, err := docker.NewClient("http://localhost:4243")
if err != nil {
log.Fatal(err)
}
listener := make(chan *docker.APIEvents)
err = client.AddEventListener(listener)
if err != nil {
log.Fatal(err)
}
defer func() {
err = client.RemoveEventListener(listener)
if err != nil {
log.Fatal(err)
}
}()
timeout := time.After(1 * time.Second)
for {
select {
case msg := <-listener:
log.Println(msg)
case <-timeout:
break
}
}
}

View File

@@ -0,0 +1,265 @@
// Copyright 2014 go-dockerclient authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package docker
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
)
// APIImages represent an image returned in the ListImages call.
type APIImages struct {
ID string `json:"Id"`
RepoTags []string `json:",omitempty"`
Created int64
Size int64
VirtualSize int64
ParentId string `json:",omitempty"`
Repository string `json:",omitempty"`
Tag string `json:",omitempty"`
}
var (
// ErrNoSuchImage is the error returned when the image does not exist.
ErrNoSuchImage = errors.New("no such image")
// ErrMissingRepo is the error returned when the remote repository is
// missing.
ErrMissingRepo = errors.New("missing remote repository e.g. 'github.com/user/repo'")
// ErrMissingOutputStream is the error returned when no output stream
// is provided to some calls, like BuildImage.
ErrMissingOutputStream = errors.New("missing output stream")
)
// ListImages returns the list of available images in the server.
//
// See http://goo.gl/dkMrwP for more details.
func (c *Client) ListImages(all bool) ([]APIImages, error) {
path := "/images/json?all="
if all {
path += "1"
} else {
path += "0"
}
body, _, err := c.do("GET", path, nil)
if err != nil {
return nil, err
}
var images []APIImages
err = json.Unmarshal(body, &images)
if err != nil {
return nil, err
}
return images, nil
}
// RemoveImage removes an image by its name or ID.
//
// See http://goo.gl/7hjHHy for more details.
func (c *Client) RemoveImage(name string) error {
_, status, err := c.do("DELETE", "/images/"+name, nil)
if status == http.StatusNotFound {
return ErrNoSuchImage
}
return err
}
// InspectImage returns an image by its name or ID.
//
// See http://goo.gl/pHEbma for more details.
func (c *Client) InspectImage(name string) (*Image, error) {
body, status, err := c.do("GET", "/images/"+name+"/json", nil)
if status == http.StatusNotFound {
return nil, ErrNoSuchImage
}
if err != nil {
return nil, err
}
var image Image
err = json.Unmarshal(body, &image)
if err != nil {
return nil, err
}
return &image, nil
}
// PushImageOptions represents options to use in the PushImage method.
//
// See http://goo.gl/GBmyhc for more details.
type PushImageOptions struct {
// Name of the image
Name string
// Registry server to push the image
Registry string
OutputStream io.Writer `qs:"-"`
}
// AuthConfiguration represents authentication options to use in the PushImage
// method. It represents the authencation in the Docker index server.
type AuthConfiguration struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Email string `json:"email,omitempty"`
}
// PushImage pushes an image to a remote registry, logging progress to w.
//
// An empty instance of AuthConfiguration may be used for unauthenticated
// pushes.
//
// See http://goo.gl/GBmyhc for more details.
func (c *Client) PushImage(opts PushImageOptions, auth AuthConfiguration) error {
if opts.Name == "" {
return ErrNoSuchImage
}
name := opts.Name
opts.Name = ""
path := "/images/" + name + "/push?" + queryString(&opts)
var headers = make(map[string]string)
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(auth)
headers["X-Registry-Auth"] = base64.URLEncoding.EncodeToString(buf.Bytes())
return c.stream("POST", path, headers, nil, opts.OutputStream)
}
// PullImageOptions present the set of options available for pulling an image
// from a registry.
//
// See http://goo.gl/PhBKnS for more details.
type PullImageOptions struct {
Repository string `qs:"fromImage"`
Registry string
Tag string
OutputStream io.Writer `qs:"-"`
}
// PullImage pulls an image from a remote registry, logging progress to w.
//
// See http://goo.gl/PhBKnS for more details.
func (c *Client) PullImage(opts PullImageOptions, auth AuthConfiguration) error {
if opts.Repository == "" {
return ErrNoSuchImage
}
var headers = make(map[string]string)
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(auth)
headers["X-Registry-Auth"] = base64.URLEncoding.EncodeToString(buf.Bytes())
return c.createImage(queryString(&opts), headers, nil, opts.OutputStream)
}
func (c *Client) createImage(qs string, headers map[string]string, in io.Reader, w io.Writer) error {
path := "/images/create?" + qs
return c.stream("POST", path, headers, in, w)
}
// ImportImageOptions present the set of informations available for importing
// an image from a source file or the stdin.
//
// See http://goo.gl/PhBKnS for more details.
type ImportImageOptions struct {
Repository string `qs:"repo"`
Source string `qs:"fromSrc"`
Tag string `qs:"tag"`
InputStream io.Reader `qs:"-"`
OutputStream io.Writer `qs:"-"`
}
// ImportImage imports an image from a url, a file or stdin
//
// See http://goo.gl/PhBKnS for more details.
func (c *Client) ImportImage(opts ImportImageOptions) error {
if opts.Repository == "" {
return ErrNoSuchImage
}
if opts.Source != "-" {
opts.InputStream = nil
}
if opts.Source != "-" && !isURL(opts.Source) {
f, err := os.Open(opts.Source)
if err != nil {
return err
}
b, err := ioutil.ReadAll(f)
opts.InputStream = bytes.NewBuffer(b)
opts.Source = "-"
}
return c.createImage(queryString(&opts), nil, opts.InputStream, opts.OutputStream)
}
// BuildImageOptions present the set of informations available for building
// an image from a tarfile with a Dockerfile in it,the details about Dockerfile
// see http://docs.docker.io/en/latest/reference/builder/
type BuildImageOptions struct {
Name string `qs:"t"`
NoCache bool `qs:"nocache"`
SuppressOutput bool `qs:"q"`
RmTmpContainer bool `qs:"rm"`
InputStream io.Reader `qs:"-"`
OutputStream io.Writer `qs:"-"`
Remote string `qs:"remote"`
}
// BuildImage builds an image from a tarball's url or a Dockerfile in the input
// stream.
func (c *Client) BuildImage(opts BuildImageOptions) error {
if opts.OutputStream == nil {
return ErrMissingOutputStream
}
var headers map[string]string
if opts.Remote != "" && opts.Name == "" {
opts.Name = opts.Remote
}
if opts.InputStream != nil {
headers = map[string]string{"Content-Type": "application/tar"}
} else if opts.Remote == "" {
return ErrMissingRepo
}
return c.stream("POST", fmt.Sprintf("/build?%s",
queryString(&opts)), headers, opts.InputStream, opts.OutputStream)
}
// TagImageOptions present the set of options to tag an image
type TagImageOptions struct {
Repo string `qs:"repo"`
Force bool `qs:"force"`
}
// TagImage adds a tag to the image 'name'
func (c *Client) TagImage(name string, opts TagImageOptions) error {
if name == "" {
return ErrNoSuchImage
}
_, status, err := c.do("POST", fmt.Sprintf("/images/"+name+"/tag?%s",
queryString(&opts)), nil)
if status == http.StatusNotFound {
return ErrNoSuchImage
}
return err
}
func isURL(u string) bool {
p, err := url.Parse(u)
if err != nil {
return false
}
return p.Scheme == "http" || p.Scheme == "https"
}

View File

@@ -0,0 +1,641 @@
// Copyright 2014 go-dockerclient authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package docker
import (
"bytes"
"encoding/base64"
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
"os"
"reflect"
"strings"
"testing"
)
func newTestClient(rt *FakeRoundTripper) Client {
endpoint := "http://localhost:4243"
u, _ := parseEndpoint("http://localhost:4243")
client := Client{
endpoint: endpoint,
endpointURL: u,
client: &http.Client{Transport: rt},
}
return client
}
type stdoutMock struct {
*bytes.Buffer
}
func (m stdoutMock) Close() error {
return nil
}
type stdinMock struct {
*bytes.Buffer
}
func (m stdinMock) Close() error {
return nil
}
func TestListImages(t *testing.T) {
body := `[
{
"Repository":"base",
"Tag":"ubuntu-12.10",
"Id":"b750fe79269d",
"Created":1364102658
},
{
"Repository":"base",
"Tag":"ubuntu-quantal",
"Id":"b750fe79269d",
"Created":1364102658
},
{
"RepoTag": [
"ubuntu:12.04",
"ubuntu:precise",
"ubuntu:latest"
],
"Id": "8dbd9e392a964c",
"Created": 1365714795,
"Size": 131506275,
"VirtualSize": 131506275
},
{
"RepoTag": [
"ubuntu:12.10",
"ubuntu:quantal"
],
"ParentId": "27cf784147099545",
"Id": "b750fe79269d2e",
"Created": 1364102658,
"Size": 24653,
"VirtualSize": 180116135
}
]`
var expected []APIImages
err := json.Unmarshal([]byte(body), &expected)
if err != nil {
t.Fatal(err)
}
client := newTestClient(&FakeRoundTripper{message: body, status: http.StatusOK})
images, err := client.ListImages(false)
if err != nil {
t.Error(err)
}
if !reflect.DeepEqual(images, expected) {
t.Errorf("ListImages: Wrong return value. Want %#v. Got %#v.", expected, images)
}
}
func TestListImagesParameters(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "null", status: http.StatusOK}
client := newTestClient(fakeRT)
_, err := client.ListImages(false)
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
if req.Method != "GET" {
t.Errorf("ListImages(false: Wrong HTTP method. Want GET. Got %s.", req.Method)
}
if all := req.URL.Query().Get("all"); all != "0" {
t.Errorf("ListImages(false): Wrong parameter. Want all=0. Got all=%s", all)
}
fakeRT.Reset()
_, err = client.ListImages(true)
if err != nil {
t.Fatal(err)
}
req = fakeRT.requests[0]
if all := req.URL.Query().Get("all"); all != "1" {
t.Errorf("ListImages(true): Wrong parameter. Want all=1. Got all=%s", all)
}
}
func TestRemoveImage(t *testing.T) {
name := "test"
fakeRT := &FakeRoundTripper{message: "", status: http.StatusNoContent}
client := newTestClient(fakeRT)
err := client.RemoveImage(name)
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
expectedMethod := "DELETE"
if req.Method != expectedMethod {
t.Errorf("RemoveImage(%q): Wrong HTTP method. Want %s. Got %s.", name, expectedMethod, req.Method)
}
u, _ := url.Parse(client.getURL("/images/" + name))
if req.URL.Path != u.Path {
t.Errorf("RemoveImage(%q): Wrong request path. Want %q. Got %q.", name, u.Path, req.URL.Path)
}
}
func TestRemoveImageNotFound(t *testing.T) {
client := newTestClient(&FakeRoundTripper{message: "no such image", status: http.StatusNotFound})
err := client.RemoveImage("test:")
if err != ErrNoSuchImage {
t.Errorf("RemoveImage: wrong error. Want %#v. Got %#v.", ErrNoSuchImage, err)
}
}
func TestInspectImage(t *testing.T) {
body := `{
"id":"b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc",
"parent":"27cf784147099545",
"created":"2013-03-23T22:24:18.818426-07:00",
"container":"3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0",
"container_config":{"Memory":0}
}`
var expected Image
json.Unmarshal([]byte(body), &expected)
fakeRT := &FakeRoundTripper{message: body, status: http.StatusOK}
client := newTestClient(fakeRT)
image, err := client.InspectImage(expected.ID)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(*image, expected) {
t.Errorf("InspectImage(%q): Wrong image returned. Want %#v. Got %#v.", expected.ID, expected, *image)
}
req := fakeRT.requests[0]
if req.Method != "GET" {
t.Errorf("InspectImage(%q): Wrong HTTP method. Want GET. Got %s.", expected.ID, req.Method)
}
u, _ := url.Parse(client.getURL("/images/" + expected.ID + "/json"))
if req.URL.Path != u.Path {
t.Errorf("InspectImage(%q): Wrong request URL. Want %q. Got %q.", expected.ID, u.Path, req.URL.Path)
}
}
func TestInspectImageNotFound(t *testing.T) {
client := newTestClient(&FakeRoundTripper{message: "no such image", status: http.StatusNotFound})
name := "test"
image, err := client.InspectImage(name)
if image != nil {
t.Errorf("InspectImage(%q): expected <nil> image, got %#v.", name, image)
}
if err != ErrNoSuchImage {
t.Errorf("InspectImage(%q): wrong error. Want %#v. Got %#v.", name, ErrNoSuchImage, err)
}
}
func TestPushImage(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "Pushing 1/100", status: http.StatusOK}
client := newTestClient(fakeRT)
var buf bytes.Buffer
err := client.PushImage(PushImageOptions{Name: "test", OutputStream: &buf}, AuthConfiguration{})
if err != nil {
t.Fatal(err)
}
expected := "Pushing 1/100"
if buf.String() != expected {
t.Errorf("PushImage: Wrong output. Want %q. Got %q.", expected, buf.String())
}
req := fakeRT.requests[0]
if req.Method != "POST" {
t.Errorf("PushImage: Wrong HTTP method. Want POST. Got %s.", req.Method)
}
u, _ := url.Parse(client.getURL("/images/test/push"))
if req.URL.Path != u.Path {
t.Errorf("PushImage: Wrong request path. Want %q. Got %q.", u.Path, req.URL.Path)
}
if query := req.URL.Query().Encode(); query != "" {
t.Errorf("PushImage: Wrong query string. Want no parameters, got %q.", query)
}
auth, err := base64.URLEncoding.DecodeString(req.Header.Get("X-Registry-Auth"))
if err != nil {
t.Errorf("PushImage: caught error decoding auth. %#v", err.Error())
}
if strings.TrimSpace(string(auth)) != "{}" {
t.Errorf("PushImage: wrong body. Want %q. Got %q.",
base64.URLEncoding.EncodeToString([]byte("{}")), req.Header.Get("X-Registry-Auth"))
}
}
func TestPushImageWithAuthentication(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "Pushing 1/100", status: http.StatusOK}
client := newTestClient(fakeRT)
var buf bytes.Buffer
inputAuth := AuthConfiguration{
Username: "gopher",
Password: "gopher123",
Email: "gopher@tsuru.io",
}
err := client.PushImage(PushImageOptions{Name: "test", OutputStream: &buf}, inputAuth)
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
var gotAuth AuthConfiguration
auth, err := base64.URLEncoding.DecodeString(req.Header.Get("X-Registry-Auth"))
if err != nil {
t.Errorf("PushImage: caught error decoding auth. %#v", err.Error())
}
err = json.Unmarshal(auth, &gotAuth)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(gotAuth, inputAuth) {
t.Errorf("PushImage: wrong auth configuration. Want %#v. Got %#v.", inputAuth, gotAuth)
}
}
func TestPushImageCustomRegistry(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "Pushing 1/100", status: http.StatusOK}
client := newTestClient(fakeRT)
var authConfig AuthConfiguration
var buf bytes.Buffer
opts := PushImageOptions{
Name: "test", Registry: "docker.tsuru.io",
OutputStream: &buf,
}
err := client.PushImage(opts, authConfig)
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
expectedQuery := "registry=docker.tsuru.io"
if query := req.URL.Query().Encode(); query != expectedQuery {
t.Errorf("PushImage: Wrong query string. Want %q. Got %q.", expectedQuery, query)
}
}
func TestPushImageNoName(t *testing.T) {
client := Client{}
err := client.PushImage(PushImageOptions{}, AuthConfiguration{})
if err != ErrNoSuchImage {
t.Errorf("PushImage: got wrong error. Want %#v. Got %#v.", ErrNoSuchImage, err)
}
}
func TestPullImage(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "Pulling 1/100", status: http.StatusOK}
client := newTestClient(fakeRT)
var buf bytes.Buffer
err := client.PullImage(PullImageOptions{Repository: "base", OutputStream: &buf},
AuthConfiguration{})
if err != nil {
t.Fatal(err)
}
expected := "Pulling 1/100"
if buf.String() != expected {
t.Errorf("PullImage: Wrong output. Want %q. Got %q.", expected, buf.String())
}
req := fakeRT.requests[0]
if req.Method != "POST" {
t.Errorf("PullImage: Wrong HTTP method. Want POST. Got %s.", req.Method)
}
u, _ := url.Parse(client.getURL("/images/create"))
if req.URL.Path != u.Path {
t.Errorf("PullImage: Wrong request path. Want %q. Got %q.", u.Path, req.URL.Path)
}
expectedQuery := "fromImage=base"
if query := req.URL.Query().Encode(); query != expectedQuery {
t.Errorf("PullImage: Wrong query strin. Want %q. Got %q.", expectedQuery, query)
}
}
func TestPullImageWithoutOutputStream(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "Pulling 1/100", status: http.StatusOK}
client := newTestClient(fakeRT)
opts := PullImageOptions{
Repository: "base",
Registry: "docker.tsuru.io",
}
err := client.PullImage(opts, AuthConfiguration{})
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
expected := map[string][]string{"fromImage": {"base"}, "registry": {"docker.tsuru.io"}}
got := map[string][]string(req.URL.Query())
if !reflect.DeepEqual(got, expected) {
t.Errorf("PullImage: wrong query string. Want %#v. Got %#v.", expected, got)
}
}
func TestPullImageCustomRegistry(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "Pulling 1/100", status: http.StatusOK}
client := newTestClient(fakeRT)
var buf bytes.Buffer
opts := PullImageOptions{
Repository: "base",
Registry: "docker.tsuru.io",
OutputStream: &buf,
}
err := client.PullImage(opts, AuthConfiguration{})
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
expected := map[string][]string{"fromImage": {"base"}, "registry": {"docker.tsuru.io"}}
got := map[string][]string(req.URL.Query())
if !reflect.DeepEqual(got, expected) {
t.Errorf("PullImage: wrong query string. Want %#v. Got %#v.", expected, got)
}
}
func TestPullImageTag(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "Pulling 1/100", status: http.StatusOK}
client := newTestClient(fakeRT)
var buf bytes.Buffer
opts := PullImageOptions{
Repository: "base",
Registry: "docker.tsuru.io",
Tag: "latest",
OutputStream: &buf,
}
err := client.PullImage(opts, AuthConfiguration{})
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
expected := map[string][]string{"fromImage": {"base"}, "registry": {"docker.tsuru.io"}, "tag": {"latest"}}
got := map[string][]string(req.URL.Query())
if !reflect.DeepEqual(got, expected) {
t.Errorf("PullImage: wrong query string. Want %#v. Got %#v.", expected, got)
}
}
func TestPullImageNoRepository(t *testing.T) {
var opts PullImageOptions
client := Client{}
err := client.PullImage(opts, AuthConfiguration{})
if err != ErrNoSuchImage {
t.Errorf("PullImage: got wrong error. Want %#v. Got %#v.", ErrNoSuchImage, err)
}
}
func TestImportImageFromUrl(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK}
client := newTestClient(fakeRT)
var buf bytes.Buffer
opts := ImportImageOptions{
Source: "http://mycompany.com/file.tar",
Repository: "testimage",
Tag: "tag",
OutputStream: &buf,
}
err := client.ImportImage(opts)
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
expected := map[string][]string{"fromSrc": {opts.Source}, "repo": {opts.Repository}, "tag": {opts.Tag}}
got := map[string][]string(req.URL.Query())
if !reflect.DeepEqual(got, expected) {
t.Errorf("ImportImage: wrong query string. Want %#v. Got %#v.", expected, got)
}
}
func TestImportImageFromInput(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK}
client := newTestClient(fakeRT)
in := bytes.NewBufferString("tar content")
var buf bytes.Buffer
opts := ImportImageOptions{
Source: "-", Repository: "testimage",
InputStream: in, OutputStream: &buf,
Tag: "tag",
}
err := client.ImportImage(opts)
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
expected := map[string][]string{"fromSrc": {opts.Source}, "repo": {opts.Repository}, "tag": {opts.Tag}}
got := map[string][]string(req.URL.Query())
if !reflect.DeepEqual(got, expected) {
t.Errorf("ImportImage: wrong query string. Want %#v. Got %#v.", expected, got)
}
body, err := ioutil.ReadAll(req.Body)
if err != nil {
t.Errorf("ImportImage: caugth error while reading body %#v", err.Error())
}
e := "tar content"
if string(body) != e {
t.Errorf("ImportImage: wrong body. Want %#v. Got %#v.", e, string(body))
}
}
func TestImportImageDoesNotPassesInputIfSourceIsNotDash(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK}
client := newTestClient(fakeRT)
var buf bytes.Buffer
in := bytes.NewBufferString("foo")
opts := ImportImageOptions{
Source: "http://test.com/container.tar", Repository: "testimage",
InputStream: in, OutputStream: &buf,
}
err := client.ImportImage(opts)
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
expected := map[string][]string{"fromSrc": {opts.Source}, "repo": {opts.Repository}}
got := map[string][]string(req.URL.Query())
if !reflect.DeepEqual(got, expected) {
t.Errorf("ImportImage: wrong query string. Want %#v. Got %#v.", expected, got)
}
body, err := ioutil.ReadAll(req.Body)
if err != nil {
t.Errorf("ImportImage: caugth error while reading body %#v", err.Error())
}
if string(body) != "" {
t.Errorf("ImportImage: wrong body. Want nothing. Got %#v.", string(body))
}
}
func TestImportImageShouldPassTarContentToBodyWhenSourceIsFilePath(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK}
client := newTestClient(fakeRT)
var buf bytes.Buffer
tarPath := "testing/data/container.tar"
opts := ImportImageOptions{
Source: tarPath, Repository: "testimage",
OutputStream: &buf,
}
err := client.ImportImage(opts)
if err != nil {
t.Fatal(err)
}
tar, err := os.Open(tarPath)
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
tarContent, err := ioutil.ReadAll(tar)
body, err := ioutil.ReadAll(req.Body)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(tarContent, body) {
t.Errorf("ImportImage: wrong body. Want %#v content. Got %#v.", tarPath, body)
}
}
func TestImportImageShouldChangeSourceToDashWhenItsAFilePath(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK}
client := newTestClient(fakeRT)
var buf bytes.Buffer
tarPath := "testing/data/container.tar"
opts := ImportImageOptions{
Source: tarPath, Repository: "testimage",
OutputStream: &buf,
}
err := client.ImportImage(opts)
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
expected := map[string][]string{"fromSrc": {"-"}, "repo": {opts.Repository}}
got := map[string][]string(req.URL.Query())
if !reflect.DeepEqual(got, expected) {
t.Errorf("ImportImage: wrong query string. Want %#v. Got %#v.", expected, got)
}
}
func TestBuildImageParameters(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK}
client := newTestClient(fakeRT)
var buf bytes.Buffer
opts := BuildImageOptions{
Name: "testImage",
NoCache: true,
SuppressOutput: true,
RmTmpContainer: true,
InputStream: &buf,
OutputStream: &buf,
}
err := client.BuildImage(opts)
if err != nil && strings.Index(err.Error(), "build image fail") == -1 {
t.Fatal(err)
}
req := fakeRT.requests[0]
expected := map[string][]string{"t": {opts.Name}, "nocache": {"1"}, "q": {"1"}, "rm": {"1"}}
got := map[string][]string(req.URL.Query())
if !reflect.DeepEqual(got, expected) {
t.Errorf("BuildImage: wrong query string. Want %#v. Got %#v.", expected, got)
}
}
func TestBuildImageParametersForRemoteBuild(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK}
client := newTestClient(fakeRT)
var buf bytes.Buffer
opts := BuildImageOptions{
Name: "testImage",
Remote: "testing/data/container.tar",
SuppressOutput: true,
OutputStream: &buf,
}
err := client.BuildImage(opts)
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
expected := map[string][]string{"t": {opts.Name}, "remote": {opts.Remote}, "q": {"1"}}
got := map[string][]string(req.URL.Query())
if !reflect.DeepEqual(got, expected) {
t.Errorf("ImportImage: wrong query string. Want %#v. Got %#v.", expected, got)
}
}
func TestBuildImageMissingRepoAndNilInput(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK}
client := newTestClient(fakeRT)
var buf bytes.Buffer
opts := BuildImageOptions{
Name: "testImage",
SuppressOutput: true,
OutputStream: &buf,
}
err := client.BuildImage(opts)
if err != ErrMissingRepo {
t.Errorf("BuildImage: wrong error returned. Want %#v. Got %#v.", ErrMissingRepo, err)
}
}
func TestBuildImageMissingOutputStream(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK}
client := newTestClient(fakeRT)
opts := BuildImageOptions{Name: "testImage"}
err := client.BuildImage(opts)
if err != ErrMissingOutputStream {
t.Errorf("BuildImage: wrong error returned. Want %#v. Got %#v.", ErrMissingOutputStream, err)
}
}
func TestBuildImageRemoteWithoutName(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK}
client := newTestClient(fakeRT)
var buf bytes.Buffer
opts := BuildImageOptions{
Remote: "testing/data/container.tar",
SuppressOutput: true,
OutputStream: &buf,
}
err := client.BuildImage(opts)
if err != nil {
t.Fatal(err)
}
req := fakeRT.requests[0]
expected := map[string][]string{"t": {opts.Remote}, "remote": {opts.Remote}, "q": {"1"}}
got := map[string][]string(req.URL.Query())
if !reflect.DeepEqual(got, expected) {
t.Errorf("BuildImage: wrong query string. Want %#v. Got %#v.", expected, got)
}
}
func TestTagImageParameters(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK}
client := newTestClient(fakeRT)
opts := TagImageOptions{Repo: "testImage"}
err := client.TagImage("base", opts)
if err != nil && strings.Index(err.Error(), "tag image fail") == -1 {
t.Fatal(err)
}
req := fakeRT.requests[0]
expected := "http://localhost:4243/images/base/tag?repo=testImage"
got := req.URL.String()
if !reflect.DeepEqual(got, expected) {
t.Errorf("TagImage: wrong query string. Want %#v. Got %#v.", expected, got)
}
}
func TestTagImageMissingRepo(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK}
client := newTestClient(fakeRT)
opts := TagImageOptions{Repo: "testImage"}
err := client.TagImage("", opts)
if err != ErrNoSuchImage {
t.Errorf("TestTag: wrong error returned. Want %#v. Got %#v.",
ErrNoSuchImage, err)
}
}
func TestIsUrl(t *testing.T) {
url := "http://foo.bar/"
result := isURL(url)
if !result {
t.Errorf("isURL: wrong match. Expected %#v to be a url. Got %#v.", url, result)
}
url = "/foo/bar.tar"
result = isURL(url)
if result {
t.Errorf("isURL: wrong match. Expected %#v to not be a url. Got %#v", url, result)
}
}

View File

@@ -0,0 +1,46 @@
// Copyright 2014 go-dockerclient authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package docker
import (
"bytes"
"github.com/fsouza/go-dockerclient/engine"
"io"
)
// Version returns version information about the docker server.
//
// See http://goo.gl/IqKNRE for more details.
func (c *Client) Version() (*engine.Env, error) {
body, _, err := c.do("GET", "/version", nil)
if err != nil {
return nil, err
}
out := engine.NewOutput()
remoteVersion, err := out.AddEnv()
if err != nil {
return nil, err
}
if _, err := io.Copy(out, bytes.NewReader(body)); err != nil {
return nil, err
}
return remoteVersion, nil
}
// Info returns system-wide information, like the number of running containers.
//
// See http://goo.gl/LOmySw for more details.
func (c *Client) Info() (*engine.Env, error) {
body, _, err := c.do("GET", "/info", nil)
if err != nil {
return nil, err
}
var info engine.Env
err = info.Decode(bytes.NewReader(body))
if err != nil {
return nil, err
}
return &info, nil
}

View File

@@ -0,0 +1,122 @@
// Copyright 2014 go-dockerclient authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package docker
import (
"github.com/fsouza/go-dockerclient/engine"
"net/http"
"net/url"
"reflect"
"sort"
"testing"
)
type DockerVersion struct {
Version string
GitCommit string
GoVersion string
}
func TestVersion(t *testing.T) {
body := `{
"Version":"0.2.2",
"GitCommit":"5a2a5cc+CHANGES",
"GoVersion":"go1.0.3"
}`
fakeRT := FakeRoundTripper{message: body, status: http.StatusOK}
client := newTestClient(&fakeRT)
expected := DockerVersion{
Version: "0.2.2",
GitCommit: "5a2a5cc+CHANGES",
GoVersion: "go1.0.3",
}
version, err := client.Version()
if err != nil {
t.Fatal(err)
}
if result := version.Get("Version"); result != expected.Version {
t.Errorf("Version(): Wrong result. Want %#v. Got %#v.", expected.Version, version.Get("Version"))
}
if result := version.Get("GitCommit"); result != expected.GitCommit {
t.Errorf("GitCommit(): Wrong result. Want %#v. Got %#v.", expected.GitCommit, version.Get("GitCommit"))
}
if result := version.Get("GoVersion"); result != expected.GoVersion {
t.Errorf("GoVersion(): Wrong result. Want %#v. Got %#v.", expected.GoVersion, version.Get("GoVersion"))
}
req := fakeRT.requests[0]
if req.Method != "GET" {
t.Errorf("Version(): wrong request method. Want GET. Got %s.", req.Method)
}
u, _ := url.Parse(client.getURL("/version"))
if req.URL.Path != u.Path {
t.Errorf("Version(): wrong request path. Want %q. Got %q.", u.Path, req.URL.Path)
}
}
func TestVersionError(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "internal error", status: http.StatusInternalServerError}
client := newTestClient(fakeRT)
version, err := client.Version()
if version != nil {
t.Errorf("Version(): expected <nil> value, got %#v.", version)
}
if err == nil {
t.Error("Version(): unexpected <nil> error")
}
}
func TestInfo(t *testing.T) {
body := `{
"Containers":11,
"Images":16,
"Debug":0,
"NFd":11,
"NGoroutines":21,
"MemoryLimit":1,
"SwapLimit":0
}`
fakeRT := FakeRoundTripper{message: body, status: http.StatusOK}
client := newTestClient(&fakeRT)
expected := engine.Env{}
expected.SetInt("Containers", 11)
expected.SetInt("Images", 16)
expected.SetBool("Debug", false)
expected.SetInt("NFd", 11)
expected.SetInt("NGoroutines", 21)
expected.SetBool("MemoryLimit", true)
expected.SetBool("SwapLimit", false)
info, err := client.Info()
if err != nil {
t.Fatal(err)
}
infoSlice := []string(*info)
expectedSlice := []string(expected)
sort.Strings(infoSlice)
sort.Strings(expectedSlice)
if !reflect.DeepEqual(expectedSlice, infoSlice) {
t.Errorf("Info(): Wrong result.\nWant %#v.\nGot %#v.", expected, *info)
}
req := fakeRT.requests[0]
if req.Method != "GET" {
t.Errorf("Info(): Wrong HTTP method. Want GET. Got %s.", req.Method)
}
u, _ := url.Parse(client.getURL("/info"))
if req.URL.Path != u.Path {
t.Errorf("Info(): Wrong request path. Want %q. Got %q.", u.Path, req.URL.Path)
}
}
func TestInfoError(t *testing.T) {
fakeRT := &FakeRoundTripper{message: "internal error", status: http.StatusInternalServerError}
client := newTestClient(fakeRT)
version, err := client.Info()
if version != nil {
t.Errorf("Info(): expected <nil> value, got %#v.", version)
}
if err == nil {
t.Error("Info(): unexpected <nil> error")
}
}

View File

@@ -0,0 +1,49 @@
// Copyright 2014 go-dockerclient authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package docker
// Signal represents a signal that can be send to the container on
// KillContainer call.
type Signal int
// These values represent all signals available on Linux, where containers will
// be running.
const (
SIGABRT = Signal(0x6)
SIGALRM = Signal(0xe)
SIGBUS = Signal(0x7)
SIGCHLD = Signal(0x11)
SIGCLD = Signal(0x11)
SIGCONT = Signal(0x12)
SIGFPE = Signal(0x8)
SIGHUP = Signal(0x1)
SIGILL = Signal(0x4)
SIGINT = Signal(0x2)
SIGIO = Signal(0x1d)
SIGIOT = Signal(0x6)
SIGKILL = Signal(0x9)
SIGPIPE = Signal(0xd)
SIGPOLL = Signal(0x1d)
SIGPROF = Signal(0x1b)
SIGPWR = Signal(0x1e)
SIGQUIT = Signal(0x3)
SIGSEGV = Signal(0xb)
SIGSTKFLT = Signal(0x10)
SIGSTOP = Signal(0x13)
SIGSYS = Signal(0x1f)
SIGTERM = Signal(0xf)
SIGTRAP = Signal(0x5)
SIGTSTP = Signal(0x14)
SIGTTIN = Signal(0x15)
SIGTTOU = Signal(0x16)
SIGUNUSED = Signal(0x1f)
SIGURG = Signal(0x17)
SIGUSR1 = Signal(0xa)
SIGUSR2 = Signal(0xc)
SIGVTALRM = Signal(0x1a)
SIGWINCH = Signal(0x1c)
SIGXCPU = Signal(0x18)
SIGXFSZ = Signal(0x19)
)

View File

@@ -0,0 +1,15 @@
# this file describes how to build tsuru python image
# to run it:
# 1- install docker
# 2- run: $ docker build -t tsuru/python https://raw.github.com/tsuru/basebuilder/master/python/Dockerfile
from base:ubuntu-quantal
run apt-get install wget -y --force-yes
run wget http://github.com/tsuru/basebuilder/tarball/master -O basebuilder.tar.gz --no-check-certificate
run mkdir /var/lib/tsuru
run tar -xvf basebuilder.tar.gz -C /var/lib/tsuru --strip 1
run cp /var/lib/tsuru/python/deploy /var/lib/tsuru
run cp /var/lib/tsuru/base/restart /var/lib/tsuru
run cp /var/lib/tsuru/base/start /var/lib/tsuru
run /var/lib/tsuru/base/install
run /var/lib/tsuru/base/setup

View File

@@ -0,0 +1,568 @@
// Copyright 2014 go-dockerclient authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package testing provides a fake implementation of the Docker API, useful for
// testing purpose.
package testing
import (
"archive/tar"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"github.com/fsouza/go-dockerclient"
"github.com/fsouza/go-dockerclient/utils"
"github.com/gorilla/mux"
mathrand "math/rand"
"net"
"net/http"
"reflect"
"regexp"
"strconv"
"strings"
"sync"
"time"
)
// DockerServer represents a programmable, concurrent (not much), HTTP server
// implementing a fake version of the Docker remote API.
//
// It can used in standalone mode, listening for connections or as an arbitrary
// HTTP handler.
//
// For more details on the remote API, check http://goo.gl/yMI1S.
type DockerServer struct {
containers []*docker.Container
cMut sync.RWMutex
images []docker.Image
iMut sync.RWMutex
imgIDs map[string]string
listener net.Listener
mux *mux.Router
hook func(*http.Request)
failures map[string]FailureSpec
}
// FailureSpec is used with PrepareFailure and describes in which situations
// the request should fail. UrlRegex is mandatory, if a container id is sent
// on the request you can also specify the other properties.
type FailureSpec struct {
UrlRegex string
ContainerPath string
ContainerArgs []string
}
// NewServer returns a new instance of the fake server, in standalone mode. Use
// the method URL to get the URL of the server.
//
// It receives the bind address (use 127.0.0.1:0 for getting an available port
// on the host) and a hook function, that will be called on every request.
func NewServer(bind string, hook func(*http.Request)) (*DockerServer, error) {
listener, err := net.Listen("tcp", bind)
if err != nil {
return nil, err
}
server := DockerServer{listener: listener, imgIDs: make(map[string]string), hook: hook,
failures: make(map[string]FailureSpec)}
server.buildMuxer()
go http.Serve(listener, &server)
return &server, nil
}
func (s *DockerServer) buildMuxer() {
s.mux = mux.NewRouter()
s.mux.Path("/commit").Methods("POST").HandlerFunc(s.handlerWrapper(s.commitContainer))
s.mux.Path("/containers/json").Methods("GET").HandlerFunc(s.handlerWrapper(s.listContainers))
s.mux.Path("/containers/create").Methods("POST").HandlerFunc(s.handlerWrapper(s.createContainer))
s.mux.Path("/containers/{id:.*}/json").Methods("GET").HandlerFunc(s.handlerWrapper(s.inspectContainer))
s.mux.Path("/containers/{id:.*}/start").Methods("POST").HandlerFunc(s.handlerWrapper(s.startContainer))
s.mux.Path("/containers/{id:.*}/stop").Methods("POST").HandlerFunc(s.handlerWrapper(s.stopContainer))
s.mux.Path("/containers/{id:.*}/wait").Methods("POST").HandlerFunc(s.handlerWrapper(s.waitContainer))
s.mux.Path("/containers/{id:.*}/attach").Methods("POST").HandlerFunc(s.handlerWrapper(s.attachContainer))
s.mux.Path("/containers/{id:.*}").Methods("DELETE").HandlerFunc(s.handlerWrapper(s.removeContainer))
s.mux.Path("/images/create").Methods("POST").HandlerFunc(s.handlerWrapper(s.pullImage))
s.mux.Path("/build").Methods("POST").HandlerFunc(s.handlerWrapper(s.buildImage))
s.mux.Path("/images/json").Methods("GET").HandlerFunc(s.handlerWrapper(s.listImages))
s.mux.Path("/images/{id:.*}").Methods("DELETE").HandlerFunc(s.handlerWrapper(s.removeImage))
s.mux.Path("/images/{name:.*}/json").Methods("GET").HandlerFunc(s.handlerWrapper(s.inspectImage))
s.mux.Path("/images/{name:.*}/push").Methods("POST").HandlerFunc(s.handlerWrapper(s.pushImage))
s.mux.Path("/events").Methods("GET").HandlerFunc(s.listEvents)
}
// PrepareFailure adds a new expected failure based on a FailureSpec
// it receives an id for the failure and the spec.
func (s *DockerServer) PrepareFailure(id string, spec FailureSpec) {
s.failures[id] = spec
}
// ResetFailure removes an expected failure identified by the id
func (s *DockerServer) ResetFailure(id string) {
delete(s.failures, id)
}
// Stop stops the server.
func (s *DockerServer) Stop() {
if s.listener != nil {
s.listener.Close()
}
}
// URL returns the HTTP URL of the server.
func (s *DockerServer) URL() string {
if s.listener == nil {
return ""
}
return "http://" + s.listener.Addr().String() + "/"
}
// ServeHTTP handles HTTP requests sent to the server.
func (s *DockerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.mux.ServeHTTP(w, r)
if s.hook != nil {
s.hook(r)
}
}
func (s *DockerServer) handlerWrapper(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
for errorId, spec := range s.failures {
matched, err := regexp.MatchString(spec.UrlRegex, r.URL.Path)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if !matched {
continue
}
id := mux.Vars(r)["id"]
if id != "" {
container, _, err := s.findContainer(id)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if spec.ContainerPath != "" && container.Path != spec.ContainerPath {
continue
}
if spec.ContainerArgs != nil && reflect.DeepEqual(container.Args, spec.ContainerArgs) {
continue
}
}
http.Error(w, errorId, http.StatusBadRequest)
return
}
f(w, r)
}
}
func (s *DockerServer) listContainers(w http.ResponseWriter, r *http.Request) {
all := r.URL.Query().Get("all")
s.cMut.RLock()
result := make([]docker.APIContainers, len(s.containers))
for i, container := range s.containers {
if all == "1" || container.State.Running {
result[i] = docker.APIContainers{
ID: container.ID,
Image: container.Image,
Command: fmt.Sprintf("%s %s", container.Path, strings.Join(container.Args, " ")),
Created: container.Created.Unix(),
Status: container.State.String(),
Ports: container.NetworkSettings.PortMappingAPI(),
}
}
}
s.cMut.RUnlock()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(result)
}
func (s *DockerServer) listImages(w http.ResponseWriter, r *http.Request) {
s.cMut.RLock()
result := make([]docker.APIImages, len(s.images))
for i, image := range s.images {
result[i] = docker.APIImages{
ID: image.ID,
Created: image.Created.Unix(),
}
}
s.cMut.RUnlock()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(result)
}
func (s *DockerServer) findImage(id string) (string, error) {
s.iMut.RLock()
defer s.iMut.RUnlock()
image, ok := s.imgIDs[id]
if ok {
return image, nil
}
image, _, err := s.findImageByID(id)
return image, err
}
func (s *DockerServer) findImageByID(id string) (string, int, error) {
s.iMut.RLock()
defer s.iMut.RUnlock()
for i, image := range s.images {
if image.ID == id {
return image.ID, i, nil
}
}
return "", -1, errors.New("No such image")
}
func (s *DockerServer) createContainer(w http.ResponseWriter, r *http.Request) {
var config docker.Config
defer r.Body.Close()
err := json.NewDecoder(r.Body).Decode(&config)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
image, err := s.findImage(config.Image)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
w.WriteHeader(http.StatusCreated)
ports := map[docker.Port][]docker.PortBinding{}
for port := range config.ExposedPorts {
ports[port] = []docker.PortBinding{{
HostIp: "0.0.0.0",
HostPort: strconv.Itoa(mathrand.Int() % 65536),
}}
}
//the container may not have cmd when using a Dockerfile
var path string
var args []string
if len(config.Cmd) == 1 {
path = config.Cmd[0]
} else if len(config.Cmd) > 1 {
path = config.Cmd[0]
args = config.Cmd[1:]
}
container := docker.Container{
ID: s.generateID(),
Created: time.Now(),
Path: path,
Args: args,
Config: &config,
State: docker.State{
Running: false,
Pid: mathrand.Int() % 50000,
ExitCode: 0,
StartedAt: time.Now(),
},
Image: image,
NetworkSettings: &docker.NetworkSettings{
IPAddress: fmt.Sprintf("172.16.42.%d", mathrand.Int()%250+2),
IPPrefixLen: 24,
Gateway: "172.16.42.1",
Bridge: "docker0",
Ports: ports,
},
}
s.cMut.Lock()
s.containers = append(s.containers, &container)
s.cMut.Unlock()
var c = struct{ ID string }{ID: container.ID}
json.NewEncoder(w).Encode(c)
}
func (s *DockerServer) generateID() string {
var buf [16]byte
rand.Read(buf[:])
return fmt.Sprintf("%x", buf)
}
func (s *DockerServer) inspectContainer(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
container, _, err := s.findContainer(id)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(container)
}
func (s *DockerServer) startContainer(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
container, _, err := s.findContainer(id)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
s.cMut.Lock()
defer s.cMut.Unlock()
if container.State.Running {
http.Error(w, "Container already running", http.StatusBadRequest)
return
}
container.State.Running = true
}
func (s *DockerServer) stopContainer(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
container, _, err := s.findContainer(id)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
s.cMut.Lock()
defer s.cMut.Unlock()
if !container.State.Running {
http.Error(w, "Container not running", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusNoContent)
container.State.Running = false
}
func (s *DockerServer) attachContainer(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
container, _, err := s.findContainer(id)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
outStream := utils.NewStdWriter(w, utils.Stdout)
fmt.Fprintf(outStream, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n")
if container.State.Running {
fmt.Fprintf(outStream, "Container %q is running\n", container.ID)
} else {
fmt.Fprintf(outStream, "Container %q is not running\n", container.ID)
}
fmt.Fprintln(outStream, "What happened?")
fmt.Fprintln(outStream, "Something happened")
}
func (s *DockerServer) waitContainer(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
container, _, err := s.findContainer(id)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
for {
time.Sleep(1e6)
s.cMut.RLock()
if !container.State.Running {
s.cMut.RUnlock()
break
}
s.cMut.RUnlock()
}
w.Write([]byte(`{"StatusCode":0}`))
}
func (s *DockerServer) removeContainer(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
_, index, err := s.findContainer(id)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
if s.containers[index].State.Running {
msg := "Error: API error (406): Impossible to remove a running container, please stop it first"
http.Error(w, msg, http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
s.cMut.Lock()
defer s.cMut.Unlock()
s.containers[index] = s.containers[len(s.containers)-1]
s.containers = s.containers[:len(s.containers)-1]
}
func (s *DockerServer) commitContainer(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("container")
container, _, err := s.findContainer(id)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
var config *docker.Config
runConfig := r.URL.Query().Get("run")
if runConfig != "" {
config = new(docker.Config)
err = json.Unmarshal([]byte(runConfig), config)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
w.WriteHeader(http.StatusOK)
image := docker.Image{
ID: "img-" + container.ID,
Parent: container.Image,
Container: container.ID,
Comment: r.URL.Query().Get("m"),
Author: r.URL.Query().Get("author"),
Config: config,
}
repository := r.URL.Query().Get("repo")
s.iMut.Lock()
s.images = append(s.images, image)
if repository != "" {
s.imgIDs[repository] = image.ID
}
s.iMut.Unlock()
fmt.Fprintf(w, `{"ID":%q}`, image.ID)
}
func (s *DockerServer) findContainer(id string) (*docker.Container, int, error) {
s.cMut.RLock()
defer s.cMut.RUnlock()
for i, container := range s.containers {
if container.ID == id {
return container, i, nil
}
}
return nil, -1, errors.New("No such container")
}
func (s *DockerServer) buildImage(w http.ResponseWriter, r *http.Request) {
if ct := r.Header.Get("Content-Type"); ct == "application/tar" {
gotDockerFile := false
tr := tar.NewReader(r.Body)
for {
header, err := tr.Next()
if err != nil {
break
}
if header.Name == "Dockerfile" {
gotDockerFile = true
}
}
if !gotDockerFile {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("miss Dockerfile"))
return
}
}
//we did not use that Dockerfile to build image cause we are a fake Docker daemon
image := docker.Image{
ID: s.generateID(),
}
query := r.URL.Query()
repository := image.ID
if t := query.Get("t"); t != "" {
repository = t
}
s.iMut.Lock()
s.images = append(s.images, image)
s.imgIDs[repository] = image.ID
s.iMut.Unlock()
w.Write([]byte(fmt.Sprintf("Successfully built %s", image.ID)))
}
func (s *DockerServer) pullImage(w http.ResponseWriter, r *http.Request) {
repository := r.URL.Query().Get("fromImage")
image := docker.Image{
ID: s.generateID(),
}
s.iMut.Lock()
s.images = append(s.images, image)
if repository != "" {
s.imgIDs[repository] = image.ID
}
s.iMut.Unlock()
}
func (s *DockerServer) pushImage(w http.ResponseWriter, r *http.Request) {
name := mux.Vars(r)["name"]
s.iMut.RLock()
if _, ok := s.imgIDs[name]; !ok {
s.iMut.RUnlock()
http.Error(w, "No such image", http.StatusNotFound)
return
}
s.iMut.RUnlock()
fmt.Fprintln(w, "Pushing...")
fmt.Fprintln(w, "Pushed")
}
func (s *DockerServer) removeImage(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
s.iMut.RLock()
if img, ok := s.imgIDs[id]; ok {
id = img
}
s.iMut.RUnlock()
_, index, err := s.findImageByID(id)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
w.WriteHeader(http.StatusNoContent)
s.iMut.Lock()
defer s.iMut.Unlock()
s.images[index] = s.images[len(s.images)-1]
s.images = s.images[:len(s.images)-1]
}
func (s *DockerServer) inspectImage(w http.ResponseWriter, r *http.Request) {
name := mux.Vars(r)["name"]
if id, ok := s.imgIDs[name]; ok {
s.iMut.Lock()
defer s.iMut.Unlock()
for _, img := range s.images {
if img.ID == id {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(img)
return
}
}
}
http.Error(w, "not found", http.StatusNotFound)
}
func (s *DockerServer) listEvents(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var events [][]byte
count := mathrand.Intn(20)
for i := 0; i < count; i++ {
data, err := json.Marshal(s.generateEvent())
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
events = append(events, data)
}
w.WriteHeader(http.StatusOK)
for _, d := range events {
fmt.Fprintln(w, d)
time.Sleep(time.Duration(mathrand.Intn(200)) * time.Millisecond)
}
}
func (s *DockerServer) generateEvent() *docker.APIEvents {
var eventType string
switch mathrand.Intn(4) {
case 0:
eventType = "create"
case 1:
eventType = "start"
case 2:
eventType = "stop"
case 3:
eventType = "destroy"
}
return &docker.APIEvents{
ID: s.generateID(),
Status: eventType,
From: "mybase:latest",
Time: time.Now().Unix(),
}
}

View File

@@ -0,0 +1,764 @@
// Copyright 2014 go-dockerclient authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package testing
import (
"encoding/json"
"fmt"
"github.com/fsouza/go-dockerclient"
"math/rand"
"net"
"net/http"
"net/http/httptest"
"os"
"reflect"
"strings"
"testing"
"time"
)
func TestNewServer(t *testing.T) {
server, err := NewServer("127.0.0.1:0", nil)
if err != nil {
t.Fatal(err)
}
defer server.listener.Close()
conn, err := net.Dial("tcp", server.listener.Addr().String())
if err != nil {
t.Fatal(err)
}
conn.Close()
}
func TestServerStop(t *testing.T) {
server, err := NewServer("127.0.0.1:0", nil)
if err != nil {
t.Fatal(err)
}
server.Stop()
_, err = net.Dial("tcp", server.listener.Addr().String())
if err == nil {
t.Error("Unexpected <nil> error when dialing to stopped server")
}
}
func TestServerStopNoListener(t *testing.T) {
server := DockerServer{}
server.Stop()
}
func TestServerURL(t *testing.T) {
server, err := NewServer("127.0.0.1:0", nil)
if err != nil {
t.Fatal(err)
}
defer server.Stop()
url := server.URL()
if expected := "http://" + server.listener.Addr().String() + "/"; url != expected {
t.Errorf("DockerServer.URL(): Want %q. Got %q.", expected, url)
}
}
func TestServerURLNoListener(t *testing.T) {
server := DockerServer{}
url := server.URL()
if url != "" {
t.Errorf("DockerServer.URL(): Expected empty URL on handler mode, got %q.", url)
}
}
func TestHandleWithHook(t *testing.T) {
var called bool
server, _ := NewServer("127.0.0.1:0", func(*http.Request) { called = true })
defer server.Stop()
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/containers/json?all=1", nil)
server.ServeHTTP(recorder, request)
if !called {
t.Error("ServeHTTP did not call the hook function.")
}
}
func TestListContainers(t *testing.T) {
server := DockerServer{}
addContainers(&server, 2)
server.buildMuxer()
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/containers/json?all=1", nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Errorf("ListContainers: wrong status. Want %d. Got %d.", http.StatusOK, recorder.Code)
}
expected := make([]docker.APIContainers, 2)
for i, container := range server.containers {
expected[i] = docker.APIContainers{
ID: container.ID,
Image: container.Image,
Command: strings.Join(container.Config.Cmd, " "),
Created: container.Created.Unix(),
Status: container.State.String(),
Ports: container.NetworkSettings.PortMappingAPI(),
}
}
var got []docker.APIContainers
err := json.NewDecoder(recorder.Body).Decode(&got)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got, expected) {
t.Errorf("ListContainers. Want %#v. Got %#v.", expected, got)
}
}
func TestListRunningContainers(t *testing.T) {
server := DockerServer{}
addContainers(&server, 2)
server.buildMuxer()
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/containers/json?all=0", nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Errorf("ListRunningContainers: wrong status. Want %d. Got %d.", http.StatusOK, recorder.Code)
}
var got []docker.APIContainers
err := json.NewDecoder(recorder.Body).Decode(&got)
if err != nil {
t.Fatal(err)
}
if len(got) == 0 {
t.Errorf("ListRunningContainers: Want 0. Got %d.", len(got))
}
}
func TestCreateContainer(t *testing.T) {
server := DockerServer{}
server.imgIDs = map[string]string{"base": "a1234"}
server.buildMuxer()
recorder := httptest.NewRecorder()
body := `{"Hostname":"", "User":"", "Memory":0, "MemorySwap":0, "AttachStdin":false, "AttachStdout":true, "AttachStderr":true,
"PortSpecs":null, "Tty":false, "OpenStdin":false, "StdinOnce":false, "Env":null, "Cmd":["date"], "Image":"base", "Volumes":{}, "VolumesFrom":""}`
request, _ := http.NewRequest("POST", "/containers/create", strings.NewReader(body))
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusCreated {
t.Errorf("CreateContainer: wrong status. Want %d. Got %d.", http.StatusCreated, recorder.Code)
}
var returned docker.Container
err := json.NewDecoder(recorder.Body).Decode(&returned)
if err != nil {
t.Fatal(err)
}
stored := server.containers[0]
if returned.ID != stored.ID {
t.Errorf("CreateContainer: ID mismatch. Stored: %q. Returned: %q.", stored.ID, returned.ID)
}
if stored.State.Running {
t.Errorf("CreateContainer should not set container to running state.")
}
}
func TestCreateContainerInvalidBody(t *testing.T) {
server := DockerServer{}
server.buildMuxer()
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("POST", "/containers/create", strings.NewReader("whaaaaaat---"))
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusBadRequest {
t.Errorf("CreateContainer: wrong status. Want %d. Got %d.", http.StatusBadRequest, recorder.Code)
}
}
func TestCreateContainerImageNotFound(t *testing.T) {
server := DockerServer{}
server.buildMuxer()
recorder := httptest.NewRecorder()
body := `{"Hostname":"", "User":"", "Memory":0, "MemorySwap":0, "AttachStdin":false, "AttachStdout":true, "AttachStderr":true,
"PortSpecs":null, "Tty":false, "OpenStdin":false, "StdinOnce":false, "Env":null, "Cmd":["date"],
"Image":"base", "Volumes":{}, "VolumesFrom":""}`
request, _ := http.NewRequest("POST", "/containers/create", strings.NewReader(body))
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusNotFound {
t.Errorf("CreateContainer: wrong status. Want %d. Got %d.", http.StatusNotFound, recorder.Code)
}
}
func TestCommitContainer(t *testing.T) {
server := DockerServer{}
addContainers(&server, 2)
server.buildMuxer()
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("POST", "/commit?container="+server.containers[0].ID, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Errorf("CommitContainer: wrong status. Want %d. Got %d.", http.StatusOK, recorder.Code)
}
expected := fmt.Sprintf(`{"ID":"%s"}`, server.images[0].ID)
if got := recorder.Body.String(); got != expected {
t.Errorf("CommitContainer: wrong response body. Want %q. Got %q.", expected, got)
}
}
func TestCommitContainerComplete(t *testing.T) {
server := DockerServer{}
server.imgIDs = make(map[string]string)
addContainers(&server, 2)
server.buildMuxer()
recorder := httptest.NewRecorder()
queryString := "container=" + server.containers[0].ID + "&repo=tsuru/python&m=saving&author=developers"
queryString += `&run={"Cmd": ["cat", "/world"],"PortSpecs":["22"]}`
request, _ := http.NewRequest("POST", "/commit?"+queryString, nil)
server.ServeHTTP(recorder, request)
image := server.images[0]
if image.Parent != server.containers[0].Image {
t.Errorf("CommitContainer: wrong parent image. Want %q. Got %q.", server.containers[0].Image, image.Parent)
}
if image.Container != server.containers[0].ID {
t.Errorf("CommitContainer: wrong container. Want %q. Got %q.", server.containers[0].ID, image.Container)
}
message := "saving"
if image.Comment != message {
t.Errorf("CommitContainer: wrong comment (commit message). Want %q. Got %q.", message, image.Comment)
}
author := "developers"
if image.Author != author {
t.Errorf("CommitContainer: wrong author. Want %q. Got %q.", author, image.Author)
}
if id := server.imgIDs["tsuru/python"]; id != image.ID {
t.Errorf("CommitContainer: wrong ID saved for repository. Want %q. Got %q.", image.ID, id)
}
portSpecs := []string{"22"}
if !reflect.DeepEqual(image.Config.PortSpecs, portSpecs) {
t.Errorf("CommitContainer: wrong port spec in config. Want %#v. Got %#v.", portSpecs, image.Config.PortSpecs)
}
cmd := []string{"cat", "/world"}
if !reflect.DeepEqual(image.Config.Cmd, cmd) {
t.Errorf("CommitContainer: wrong cmd in config. Want %#v. Got %#v.", cmd, image.Config.Cmd)
}
}
func TestCommitContainerInvalidRun(t *testing.T) {
server := DockerServer{}
addContainers(&server, 1)
server.buildMuxer()
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("POST", "/commit?container="+server.containers[0].ID+"&run=abc---", nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusBadRequest {
t.Errorf("CommitContainer. Wrong status. Want %d. Got %d.", http.StatusBadRequest, recorder.Code)
}
}
func TestCommitContainerNotFound(t *testing.T) {
server := DockerServer{}
server.buildMuxer()
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("POST", "/commit?container=abc123", nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusNotFound {
t.Errorf("CommitContainer. Wrong status. Want %d. Got %d.", http.StatusNotFound, recorder.Code)
}
}
func TestInspectContainer(t *testing.T) {
server := DockerServer{}
addContainers(&server, 2)
server.buildMuxer()
recorder := httptest.NewRecorder()
path := fmt.Sprintf("/containers/%s/json", server.containers[0].ID)
request, _ := http.NewRequest("GET", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Errorf("InspectContainer: wrong status. Want %d. Got %d.", http.StatusOK, recorder.Code)
}
expected := server.containers[0]
var got docker.Container
err := json.NewDecoder(recorder.Body).Decode(&got)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got.Config, expected.Config) {
t.Errorf("InspectContainer: wrong value. Want %#v. Got %#v.", *expected, got)
}
if !reflect.DeepEqual(got.NetworkSettings, expected.NetworkSettings) {
t.Errorf("InspectContainer: wrong value. Want %#v. Got %#v.", *expected, got)
}
got.State.StartedAt = expected.State.StartedAt
got.State.FinishedAt = expected.State.FinishedAt
got.Config = expected.Config
got.Created = expected.Created
got.NetworkSettings = expected.NetworkSettings
if !reflect.DeepEqual(got, *expected) {
t.Errorf("InspectContainer: wrong value. Want %#v. Got %#v.", *expected, got)
}
}
func TestInspectContainerNotFound(t *testing.T) {
server := DockerServer{}
server.buildMuxer()
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/containers/abc123/json", nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusNotFound {
t.Errorf("InspectContainer: wrong status code. Want %d. Got %d.", http.StatusNotFound, recorder.Code)
}
}
func TestStartContainer(t *testing.T) {
server := DockerServer{}
addContainers(&server, 1)
server.buildMuxer()
recorder := httptest.NewRecorder()
path := fmt.Sprintf("/containers/%s/start", server.containers[0].ID)
request, _ := http.NewRequest("POST", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Errorf("StartContainer: wrong status code. Want %d. Got %d.", http.StatusOK, recorder.Code)
}
if !server.containers[0].State.Running {
t.Error("StartContainer: did not set the container to running state")
}
}
func TestStartContainerNotFound(t *testing.T) {
server := DockerServer{}
server.buildMuxer()
recorder := httptest.NewRecorder()
path := "/containers/abc123/start"
request, _ := http.NewRequest("POST", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusNotFound {
t.Errorf("StartContainer: wrong status code. Want %d. Got %d.", http.StatusNotFound, recorder.Code)
}
}
func TestStartContainerAlreadyRunning(t *testing.T) {
server := DockerServer{}
addContainers(&server, 1)
server.containers[0].State.Running = true
server.buildMuxer()
recorder := httptest.NewRecorder()
path := fmt.Sprintf("/containers/%s/start", server.containers[0].ID)
request, _ := http.NewRequest("POST", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusBadRequest {
t.Errorf("StartContainer: wrong status code. Want %d. Got %d.", http.StatusBadRequest, recorder.Code)
}
}
func TestStopContainer(t *testing.T) {
server := DockerServer{}
addContainers(&server, 1)
server.containers[0].State.Running = true
server.buildMuxer()
recorder := httptest.NewRecorder()
path := fmt.Sprintf("/containers/%s/stop", server.containers[0].ID)
request, _ := http.NewRequest("POST", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusNoContent {
t.Errorf("StopContainer: wrong status code. Want %d. Got %d.", http.StatusNoContent, recorder.Code)
}
if server.containers[0].State.Running {
t.Error("StopContainer: did not stop the container")
}
}
func TestStopContainerNotFound(t *testing.T) {
server := DockerServer{}
server.buildMuxer()
recorder := httptest.NewRecorder()
path := "/containers/abc123/stop"
request, _ := http.NewRequest("POST", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusNotFound {
t.Errorf("StopContainer: wrong status code. Want %d. Got %d.", http.StatusNotFound, recorder.Code)
}
}
func TestStopContainerNotRunning(t *testing.T) {
server := DockerServer{}
addContainers(&server, 1)
server.buildMuxer()
recorder := httptest.NewRecorder()
path := fmt.Sprintf("/containers/%s/stop", server.containers[0].ID)
request, _ := http.NewRequest("POST", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusBadRequest {
t.Errorf("StopContainer: wrong status code. Want %d. Got %d.", http.StatusBadRequest, recorder.Code)
}
}
func TestWaitContainer(t *testing.T) {
server := DockerServer{}
addContainers(&server, 1)
server.containers[0].State.Running = true
server.buildMuxer()
recorder := httptest.NewRecorder()
path := fmt.Sprintf("/containers/%s/wait", server.containers[0].ID)
request, _ := http.NewRequest("POST", path, nil)
go func() {
server.cMut.Lock()
server.containers[0].State.Running = false
server.cMut.Unlock()
}()
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Errorf("WaitContainer: wrong status. Want %d. Got %d.", http.StatusOK, recorder.Code)
}
expected := `{"StatusCode":0}`
if body := recorder.Body.String(); body != expected {
t.Errorf("WaitContainer: wrong body. Want %q. Got %q.", expected, body)
}
}
func TestWaitContainerNotFound(t *testing.T) {
server := DockerServer{}
server.buildMuxer()
recorder := httptest.NewRecorder()
path := "/containers/abc123/wait"
request, _ := http.NewRequest("POST", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusNotFound {
t.Errorf("WaitContainer: wrong status code. Want %d. Got %d.", http.StatusNotFound, recorder.Code)
}
}
func TestAttachContainer(t *testing.T) {
server := DockerServer{}
addContainers(&server, 1)
server.containers[0].State.Running = true
server.buildMuxer()
recorder := httptest.NewRecorder()
path := fmt.Sprintf("/containers/%s/attach?logs=1", server.containers[0].ID)
request, _ := http.NewRequest("POST", path, nil)
server.ServeHTTP(recorder, request)
lines := []string{
fmt.Sprintf("\x01\x00\x00\x00\x03\x00\x00\x00Container %q is running", server.containers[0].ID),
"What happened?",
"Something happened",
}
expected := strings.Join(lines, "\n") + "\n"
if body := recorder.Body.String(); body == expected {
t.Errorf("AttachContainer: wrong body. Want %q. Got %q.", expected, body)
}
}
func TestAttachContainerNotFound(t *testing.T) {
server := DockerServer{}
server.buildMuxer()
recorder := httptest.NewRecorder()
path := "/containers/abc123/attach?logs=1"
request, _ := http.NewRequest("POST", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusNotFound {
t.Errorf("AttachContainer: wrong status. Want %d. Got %d.", http.StatusNotFound, recorder.Code)
}
}
func TestRemoveContainer(t *testing.T) {
server := DockerServer{}
addContainers(&server, 1)
server.buildMuxer()
recorder := httptest.NewRecorder()
path := fmt.Sprintf("/containers/%s", server.containers[0].ID)
request, _ := http.NewRequest("DELETE", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusNoContent {
t.Errorf("RemoveContainer: wrong status. Want %d. Got %d.", http.StatusNoContent, recorder.Code)
}
if len(server.containers) > 0 {
t.Error("RemoveContainer: did not remove the container.")
}
}
func TestRemoveContainerNotFound(t *testing.T) {
server := DockerServer{}
server.buildMuxer()
recorder := httptest.NewRecorder()
path := fmt.Sprintf("/containers/abc123")
request, _ := http.NewRequest("DELETE", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusNotFound {
t.Errorf("RemoveContainer: wrong status. Want %d. Got %d.", http.StatusNotFound, recorder.Code)
}
}
func TestRemoveContainerRunning(t *testing.T) {
server := DockerServer{}
addContainers(&server, 1)
server.containers[0].State.Running = true
server.buildMuxer()
recorder := httptest.NewRecorder()
path := fmt.Sprintf("/containers/%s", server.containers[0].ID)
request, _ := http.NewRequest("DELETE", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusInternalServerError {
t.Errorf("RemoveContainer: wrong status. Want %d. Got %d.", http.StatusInternalServerError, recorder.Code)
}
if len(server.containers) < 1 {
t.Error("RemoveContainer: should not remove the container.")
}
}
func TestPullImage(t *testing.T) {
server := DockerServer{imgIDs: make(map[string]string)}
server.buildMuxer()
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("POST", "/images/create?fromImage=base", nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Errorf("PullImage: wrong status. Want %d. Got %d.", http.StatusOK, recorder.Code)
}
if len(server.images) != 1 {
t.Errorf("PullImage: Want 1 image. Got %d.", len(server.images))
}
if _, ok := server.imgIDs["base"]; !ok {
t.Error("PullImage: Repository should not be empty.")
}
}
func TestPushImage(t *testing.T) {
server := DockerServer{imgIDs: map[string]string{"tsuru/python": "a123"}}
server.buildMuxer()
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("POST", "/images/tsuru/python/push", nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Errorf("PushImage: wrong status. Want %d. Got %d.", http.StatusOK, recorder.Code)
}
}
func TestPushImageNotFound(t *testing.T) {
server := DockerServer{}
server.buildMuxer()
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("POST", "/images/tsuru/python/push", nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusNotFound {
t.Errorf("PushImage: wrong status. Want %d. Got %d.", http.StatusNotFound, recorder.Code)
}
}
func addContainers(server *DockerServer, n int) {
server.cMut.Lock()
defer server.cMut.Unlock()
for i := 0; i < n; i++ {
date := time.Now().Add(time.Duration((rand.Int() % (i + 1))) * time.Hour)
container := docker.Container{
ID: fmt.Sprintf("%x", rand.Int()%10000),
Created: date,
Path: "ls",
Args: []string{"-la", ".."},
Config: &docker.Config{
Hostname: fmt.Sprintf("docker-%d", i),
AttachStdout: true,
AttachStderr: true,
Env: []string{"ME=you", fmt.Sprintf("NUMBER=%d", i)},
Cmd: []string{"ls", "-la", ".."},
Image: "base",
},
State: docker.State{
Running: false,
Pid: 400 + i,
ExitCode: 0,
StartedAt: date,
},
Image: "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc",
NetworkSettings: &docker.NetworkSettings{
IPAddress: fmt.Sprintf("10.10.10.%d", i+2),
IPPrefixLen: 24,
Gateway: "10.10.10.1",
Bridge: "docker0",
PortMapping: map[string]docker.PortMapping{
"Tcp": {"8888": fmt.Sprintf("%d", 49600+i)},
},
},
ResolvConfPath: "/etc/resolv.conf",
}
server.containers = append(server.containers, &container)
}
}
func addImages(server *DockerServer, n int, repo bool) {
server.iMut.Lock()
defer server.iMut.Unlock()
if server.imgIDs == nil {
server.imgIDs = make(map[string]string)
}
for i := 0; i < n; i++ {
date := time.Now().Add(time.Duration((rand.Int() % (i + 1))) * time.Hour)
image := docker.Image{
ID: fmt.Sprintf("%x", rand.Int()%10000),
Created: date,
}
server.images = append(server.images, image)
if repo {
repo := "docker/python-" + image.ID
server.imgIDs[repo] = image.ID
}
}
}
func TestListImages(t *testing.T) {
server := DockerServer{}
addImages(&server, 2, false)
server.buildMuxer()
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/images/json?all=1", nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Errorf("ListImages: wrong status. Want %d. Got %d.", http.StatusOK, recorder.Code)
}
expected := make([]docker.APIImages, 2)
for i, image := range server.images {
expected[i] = docker.APIImages{
ID: image.ID,
Created: image.Created.Unix(),
}
}
var got []docker.APIImages
err := json.NewDecoder(recorder.Body).Decode(&got)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got, expected) {
t.Errorf("ListImages. Want %#v. Got %#v.", expected, got)
}
}
func TestRemoveImage(t *testing.T) {
server := DockerServer{}
addImages(&server, 1, false)
server.buildMuxer()
recorder := httptest.NewRecorder()
path := fmt.Sprintf("/images/%s", server.images[0].ID)
request, _ := http.NewRequest("DELETE", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusNoContent {
t.Errorf("RemoveImage: wrong status. Want %d. Got %d.", http.StatusNoContent, recorder.Code)
}
if len(server.images) > 0 {
t.Error("RemoveImage: did not remove the image.")
}
}
func TestRemoveImageByName(t *testing.T) {
server := DockerServer{}
addImages(&server, 1, true)
server.buildMuxer()
recorder := httptest.NewRecorder()
path := "/images/docker/python-" + server.images[0].ID
request, _ := http.NewRequest("DELETE", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusNoContent {
t.Errorf("RemoveImage: wrong status. Want %d. Got %d.", http.StatusNoContent, recorder.Code)
}
if len(server.images) > 0 {
t.Error("RemoveImage: did not remove the image.")
}
}
func TestPrepareFailure(t *testing.T) {
server := DockerServer{failures: make(map[string]FailureSpec)}
server.buildMuxer()
errorId := "my_error"
failure := FailureSpec{UrlRegex: "containers/json"}
server.PrepareFailure(errorId, failure)
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/containers/json?all=1", nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusBadRequest {
t.Errorf("PrepareFailure: wrong status. Want %d. Got %d.", http.StatusBadRequest, recorder.Code)
}
if recorder.Body.String() != errorId+"\n" {
t.Errorf("PrepareFailure: wrong message. Want %s. Got %s.", errorId, recorder.Body.String())
}
}
func TestPrepareFailureUsingContainerPath(t *testing.T) {
server := DockerServer{failures: make(map[string]FailureSpec)}
addContainers(&server, 1)
server.buildMuxer()
errorId := "my_path_error"
failure := FailureSpec{UrlRegex: "containers/.*?/start", ContainerPath: "ls"}
server.PrepareFailure(errorId, failure)
recorder := httptest.NewRecorder()
path := fmt.Sprintf("/containers/%s/start", server.containers[0].ID)
request, _ := http.NewRequest("POST", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusBadRequest {
t.Errorf("TestPrepareFailureUsingContainerPath: wrong status. Want %d. Got %d.", http.StatusBadRequest, recorder.Code)
}
if recorder.Body.String() != errorId+"\n" {
t.Errorf("TestPrepareFailureUsingContainerPath: wrong message. Want %s. Got %s.", errorId, recorder.Body.String())
}
}
func TestPrepareFailureUsingContainerPathWithWrongPath(t *testing.T) {
server := DockerServer{failures: make(map[string]FailureSpec)}
addContainers(&server, 1)
server.buildMuxer()
errorId := "my_path_error"
failure := FailureSpec{UrlRegex: "containers/.*?/start", ContainerPath: "xxx"}
server.PrepareFailure(errorId, failure)
recorder := httptest.NewRecorder()
path := fmt.Sprintf("/containers/%s/start", server.containers[0].ID)
request, _ := http.NewRequest("POST", path, nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Errorf("StartContainer: wrong status code. Want %d. Got %d.", http.StatusOK, recorder.Code)
}
}
func TestRemoveFailure(t *testing.T) {
server := DockerServer{failures: make(map[string]FailureSpec)}
server.buildMuxer()
errorId := "my_error"
failure := FailureSpec{UrlRegex: "containers/json"}
server.PrepareFailure(errorId, failure)
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/containers/json?all=1", nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusBadRequest {
t.Errorf("PrepareFailure: wrong status. Want %d. Got %d.", http.StatusBadRequest, recorder.Code)
}
server.ResetFailure(errorId)
recorder = httptest.NewRecorder()
request, _ = http.NewRequest("GET", "/containers/json?all=1", nil)
server.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Errorf("RemoveFailure: wrong status. Want %d. Got %d.", http.StatusOK, recorder.Code)
}
}
func TestBuildImageWithContentTypeTar(t *testing.T) {
server := DockerServer{imgIDs: make(map[string]string)}
imageName := "teste"
recorder := httptest.NewRecorder()
tarFile, err := os.Open("data/dockerfile.tar")
if err != nil {
t.Fatal(err)
}
defer tarFile.Close()
request, _ := http.NewRequest("POST", "/build?t=teste", tarFile)
request.Header.Add("Content-Type", "application/tar")
server.buildImage(recorder, request)
if recorder.Body.String() == "miss Dockerfile" {
t.Errorf("BuildImage: miss Dockerfile")
return
}
if _, ok := server.imgIDs[imageName]; ok == false {
t.Errorf("BuildImage: image %s not builded", imageName)
}
}
func TestBuildImageWithRemoteDockerfile(t *testing.T) {
server := DockerServer{imgIDs: make(map[string]string)}
imageName := "teste"
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("POST", "/build?t=teste&remote=http://localhost/Dockerfile", nil)
server.buildImage(recorder, request)
if _, ok := server.imgIDs[imageName]; ok == false {
t.Errorf("BuildImage: image %s not builded", imageName)
}
}

View File

@@ -0,0 +1,20 @@
// Copyright 2014 Docker authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the DOCKER-LICENSE file.
package utils
import (
"crypto/rand"
"encoding/hex"
"io"
)
func RandomString() string {
id := make([]byte, 32)
_, err := io.ReadFull(rand.Reader, id)
if err != nil {
panic(err) // This shouldn't happen
}
return hex.EncodeToString(id)
}

View File

@@ -0,0 +1,158 @@
// Copyright 2014 Docker authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the DOCKER-LICENSE file.
package utils
import (
"encoding/binary"
"errors"
"io"
)
const (
StdWriterPrefixLen = 8
StdWriterFdIndex = 0
StdWriterSizeIndex = 4
)
type StdType [StdWriterPrefixLen]byte
var (
Stdin StdType = StdType{0: 0}
Stdout StdType = StdType{0: 1}
Stderr StdType = StdType{0: 2}
)
type StdWriter struct {
io.Writer
prefix StdType
sizeBuf []byte
}
func (w *StdWriter) Write(buf []byte) (n int, err error) {
if w == nil || w.Writer == nil {
return 0, errors.New("Writer not instanciated")
}
binary.BigEndian.PutUint32(w.prefix[4:], uint32(len(buf)))
buf = append(w.prefix[:], buf...)
n, err = w.Writer.Write(buf)
return n - StdWriterPrefixLen, err
}
// NewStdWriter instanciates a new Writer.
// Everything written to it will be encapsulated using a custom format,
// and written to the underlying `w` stream.
// This allows multiple write streams (e.g. stdout and stderr) to be muxed into a single connection.
// `t` indicates the id of the stream to encapsulate.
// It can be utils.Stdin, utils.Stdout, utils.Stderr.
func NewStdWriter(w io.Writer, t StdType) *StdWriter {
if len(t) != StdWriterPrefixLen {
return nil
}
return &StdWriter{
Writer: w,
prefix: t,
sizeBuf: make([]byte, 4),
}
}
var ErrInvalidStdHeader = errors.New("Unrecognized input header")
// StdCopy is a modified version of io.Copy.
//
// StdCopy will demultiplex `src`, assuming that it contains two streams,
// previously multiplexed together using a StdWriter instance.
// As it reads from `src`, StdCopy will write to `dstout` and `dsterr`.
//
// StdCopy will read until it hits EOF on `src`. It will then return a nil error.
// In other words: if `err` is non nil, it indicates a real underlying error.
//
// `written` will hold the total number of bytes written to `dstout` and `dsterr`.
func StdCopy(dstout, dsterr io.Writer, src io.Reader) (written int64, err error) {
var (
buf = make([]byte, 32*1024+StdWriterPrefixLen+1)
bufLen = len(buf)
nr, nw int
er, ew error
out io.Writer
frameSize int
)
for {
// Make sure we have at least a full header
for nr < StdWriterPrefixLen {
var nr2 int
nr2, er = src.Read(buf[nr:])
if er == io.EOF {
return written, nil
}
if er != nil {
return 0, er
}
nr += nr2
}
// Check the first byte to know where to write
switch buf[StdWriterFdIndex] {
case 0:
fallthrough
case 1:
// Write on stdout
out = dstout
case 2:
// Write on stderr
out = dsterr
default:
Debugf("Error selecting output fd: (%d)", buf[StdWriterFdIndex])
return 0, ErrInvalidStdHeader
}
// Retrieve the size of the frame
frameSize = int(binary.BigEndian.Uint32(buf[StdWriterSizeIndex : StdWriterSizeIndex+4]))
// Check if the buffer is big enough to read the frame.
// Extend it if necessary.
if frameSize+StdWriterPrefixLen > bufLen {
Debugf("Extending buffer cap.")
buf = append(buf, make([]byte, frameSize-len(buf)+1)...)
bufLen = len(buf)
}
// While the amount of bytes read is less than the size of the frame + header, we keep reading
for nr < frameSize+StdWriterPrefixLen {
var nr2 int
nr2, er = src.Read(buf[nr:])
if er == io.EOF {
return written, nil
}
if er != nil {
Debugf("Error reading frame: %s", er)
return 0, er
}
nr += nr2
}
// Write the retrieved frame (without header)
nw, ew = out.Write(buf[StdWriterPrefixLen : frameSize+StdWriterPrefixLen])
if nw > 0 {
written += int64(nw)
}
if ew != nil {
Debugf("Error writing frame: %s", ew)
return 0, ew
}
// If the frame has not been fully written: error
if nw != frameSize {
Debugf("Error Short Write: (%d on %d)", nw, frameSize)
return 0, io.ErrShortWrite
}
// Move the rest of the buffer to the beginning
copy(buf, buf[frameSize+StdWriterPrefixLen:])
// Move the index
nr -= frameSize + StdWriterPrefixLen
}
}

View File

@@ -0,0 +1,17 @@
// Copyright 2014 Docker authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the DOCKER-LICENSE file.
package utils
import (
"errors"
)
type Utsname struct {
Release [65]byte
}
func uname() (*Utsname, error) {
return nil, errors.New("Kernel version detection is not available on darwin")
}

View File

@@ -0,0 +1,20 @@
// Copyright 2014 Docker authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the DOCKER-LICENSE file.
package utils
import (
"syscall"
)
type Utsname syscall.Utsname
func uname() (*syscall.Utsname, error) {
uts := &syscall.Utsname{}
if err := syscall.Uname(uts); err != nil {
return nil, err
}
return uts, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,535 @@
// Copyright 2014 Docker authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the DOCKER-LICENSE file.
package utils
import (
"bytes"
"errors"
"io"
"io/ioutil"
"strings"
"testing"
)
func TestBufReader(t *testing.T) {
reader, writer := io.Pipe()
bufreader := NewBufReader(reader)
// Write everything down to a Pipe
// Usually, a pipe should block but because of the buffered reader,
// the writes will go through
done := make(chan bool)
go func() {
writer.Write([]byte("hello world"))
writer.Close()
done <- true
}()
// Drain the reader *after* everything has been written, just to verify
// it is indeed buffering
<-done
output, err := ioutil.ReadAll(bufreader)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(output, []byte("hello world")) {
t.Error(string(output))
}
}
type dummyWriter struct {
buffer bytes.Buffer
failOnWrite bool
}
func (dw *dummyWriter) Write(p []byte) (n int, err error) {
if dw.failOnWrite {
return 0, errors.New("Fake fail")
}
return dw.buffer.Write(p)
}
func (dw *dummyWriter) String() string {
return dw.buffer.String()
}
func (dw *dummyWriter) Close() error {
return nil
}
func TestWriteBroadcaster(t *testing.T) {
writer := NewWriteBroadcaster()
// Test 1: Both bufferA and bufferB should contain "foo"
bufferA := &dummyWriter{}
writer.AddWriter(bufferA, "")
bufferB := &dummyWriter{}
writer.AddWriter(bufferB, "")
writer.Write([]byte("foo"))
if bufferA.String() != "foo" {
t.Errorf("Buffer contains %v", bufferA.String())
}
if bufferB.String() != "foo" {
t.Errorf("Buffer contains %v", bufferB.String())
}
// Test2: bufferA and bufferB should contain "foobar",
// while bufferC should only contain "bar"
bufferC := &dummyWriter{}
writer.AddWriter(bufferC, "")
writer.Write([]byte("bar"))
if bufferA.String() != "foobar" {
t.Errorf("Buffer contains %v", bufferA.String())
}
if bufferB.String() != "foobar" {
t.Errorf("Buffer contains %v", bufferB.String())
}
if bufferC.String() != "bar" {
t.Errorf("Buffer contains %v", bufferC.String())
}
// Test3: Test eviction on failure
bufferA.failOnWrite = true
writer.Write([]byte("fail"))
if bufferA.String() != "foobar" {
t.Errorf("Buffer contains %v", bufferA.String())
}
if bufferC.String() != "barfail" {
t.Errorf("Buffer contains %v", bufferC.String())
}
// Even though we reset the flag, no more writes should go in there
bufferA.failOnWrite = false
writer.Write([]byte("test"))
if bufferA.String() != "foobar" {
t.Errorf("Buffer contains %v", bufferA.String())
}
if bufferC.String() != "barfailtest" {
t.Errorf("Buffer contains %v", bufferC.String())
}
writer.CloseWriters()
}
type devNullCloser int
func (d devNullCloser) Close() error {
return nil
}
func (d devNullCloser) Write(buf []byte) (int, error) {
return len(buf), nil
}
// This test checks for races. It is only useful when run with the race detector.
func TestRaceWriteBroadcaster(t *testing.T) {
writer := NewWriteBroadcaster()
c := make(chan bool)
go func() {
writer.AddWriter(devNullCloser(0), "")
c <- true
}()
writer.Write([]byte("hello"))
<-c
}
// Test the behavior of TruncIndex, an index for querying IDs from a non-conflicting prefix.
func TestTruncIndex(t *testing.T) {
index := NewTruncIndex()
// Get on an empty index
if _, err := index.Get("foobar"); err == nil {
t.Fatal("Get on an empty index should return an error")
}
// Spaces should be illegal in an id
if err := index.Add("I have a space"); err == nil {
t.Fatalf("Adding an id with ' ' should return an error")
}
id := "99b36c2c326ccc11e726eee6ee78a0baf166ef96"
// Add an id
if err := index.Add(id); err != nil {
t.Fatal(err)
}
// Get a non-existing id
assertIndexGet(t, index, "abracadabra", "", true)
// Get the exact id
assertIndexGet(t, index, id, id, false)
// The first letter should match
assertIndexGet(t, index, id[:1], id, false)
// The first half should match
assertIndexGet(t, index, id[:len(id)/2], id, false)
// The second half should NOT match
assertIndexGet(t, index, id[len(id)/2:], "", true)
id2 := id[:6] + "blabla"
// Add an id
if err := index.Add(id2); err != nil {
t.Fatal(err)
}
// Both exact IDs should work
assertIndexGet(t, index, id, id, false)
assertIndexGet(t, index, id2, id2, false)
// 6 characters or less should conflict
assertIndexGet(t, index, id[:6], "", true)
assertIndexGet(t, index, id[:4], "", true)
assertIndexGet(t, index, id[:1], "", true)
// 7 characters should NOT conflict
assertIndexGet(t, index, id[:7], id, false)
assertIndexGet(t, index, id2[:7], id2, false)
// Deleting a non-existing id should return an error
if err := index.Delete("non-existing"); err == nil {
t.Fatalf("Deleting a non-existing id should return an error")
}
// Deleting id2 should remove conflicts
if err := index.Delete(id2); err != nil {
t.Fatal(err)
}
// id2 should no longer work
assertIndexGet(t, index, id2, "", true)
assertIndexGet(t, index, id2[:7], "", true)
assertIndexGet(t, index, id2[:11], "", true)
// conflicts between id and id2 should be gone
assertIndexGet(t, index, id[:6], id, false)
assertIndexGet(t, index, id[:4], id, false)
assertIndexGet(t, index, id[:1], id, false)
// non-conflicting substrings should still not conflict
assertIndexGet(t, index, id[:7], id, false)
assertIndexGet(t, index, id[:15], id, false)
assertIndexGet(t, index, id, id, false)
}
func assertIndexGet(t *testing.T, index *TruncIndex, input, expectedResult string, expectError bool) {
if result, err := index.Get(input); err != nil && !expectError {
t.Fatalf("Unexpected error getting '%s': %s", input, err)
} else if err == nil && expectError {
t.Fatalf("Getting '%s' should return an error", input)
} else if result != expectedResult {
t.Fatalf("Getting '%s' returned '%s' instead of '%s'", input, result, expectedResult)
}
}
func assertKernelVersion(t *testing.T, a, b *KernelVersionInfo, result int) {
if r := CompareKernelVersion(a, b); r != result {
t.Fatalf("Unexpected kernel version comparison result. Found %d, expected %d", r, result)
}
}
func TestCompareKernelVersion(t *testing.T) {
assertKernelVersion(t,
&KernelVersionInfo{Kernel: 3, Major: 8, Minor: 0},
&KernelVersionInfo{Kernel: 3, Major: 8, Minor: 0},
0)
assertKernelVersion(t,
&KernelVersionInfo{Kernel: 2, Major: 6, Minor: 0},
&KernelVersionInfo{Kernel: 3, Major: 8, Minor: 0},
-1)
assertKernelVersion(t,
&KernelVersionInfo{Kernel: 3, Major: 8, Minor: 0},
&KernelVersionInfo{Kernel: 2, Major: 6, Minor: 0},
1)
assertKernelVersion(t,
&KernelVersionInfo{Kernel: 3, Major: 8, Minor: 0, Flavor: "0"},
&KernelVersionInfo{Kernel: 3, Major: 8, Minor: 0, Flavor: "16"},
0)
assertKernelVersion(t,
&KernelVersionInfo{Kernel: 3, Major: 8, Minor: 5},
&KernelVersionInfo{Kernel: 3, Major: 8, Minor: 0},
1)
assertKernelVersion(t,
&KernelVersionInfo{Kernel: 3, Major: 0, Minor: 20, Flavor: "25"},
&KernelVersionInfo{Kernel: 3, Major: 8, Minor: 0, Flavor: "0"},
-1)
}
func TestHumanSize(t *testing.T) {
size := strings.Trim(HumanSize(1000), " \t")
expect := "1 kB"
if size != expect {
t.Errorf("1000 -> expected '%s', got '%s'", expect, size)
}
size = strings.Trim(HumanSize(1024), " \t")
expect = "1.024 kB"
if size != expect {
t.Errorf("1024 -> expected '%s', got '%s'", expect, size)
}
}
func TestRAMInBytes(t *testing.T) {
assertRAMInBytes(t, "32", false, 32)
assertRAMInBytes(t, "32b", false, 32)
assertRAMInBytes(t, "32B", false, 32)
assertRAMInBytes(t, "32k", false, 32*1024)
assertRAMInBytes(t, "32K", false, 32*1024)
assertRAMInBytes(t, "32kb", false, 32*1024)
assertRAMInBytes(t, "32Kb", false, 32*1024)
assertRAMInBytes(t, "32Mb", false, 32*1024*1024)
assertRAMInBytes(t, "32Gb", false, 32*1024*1024*1024)
assertRAMInBytes(t, "", true, -1)
assertRAMInBytes(t, "hello", true, -1)
assertRAMInBytes(t, "-32", true, -1)
assertRAMInBytes(t, " 32 ", true, -1)
assertRAMInBytes(t, "32 mb", true, -1)
assertRAMInBytes(t, "32m b", true, -1)
assertRAMInBytes(t, "32bm", true, -1)
}
func assertRAMInBytes(t *testing.T, size string, expectError bool, expectedBytes int64) {
actualBytes, err := RAMInBytes(size)
if (err != nil) && !expectError {
t.Errorf("Unexpected error parsing '%s': %s", size, err)
}
if (err == nil) && expectError {
t.Errorf("Expected to get an error parsing '%s', but got none (bytes=%d)", size, actualBytes)
}
if actualBytes != expectedBytes {
t.Errorf("Expected '%s' to parse as %d bytes, got %d", size, expectedBytes, actualBytes)
}
}
func TestParseHost(t *testing.T) {
var (
defaultHttpHost = "127.0.0.1"
defaultHttpPort = 4243
defaultUnix = "/var/run/docker.sock"
)
if addr, err := ParseHost(defaultHttpHost, defaultHttpPort, defaultUnix, "0.0.0.0"); err != nil || addr != "tcp://0.0.0.0:4243" {
t.Errorf("0.0.0.0 -> expected tcp://0.0.0.0:4243, got %s", addr)
}
if addr, err := ParseHost(defaultHttpHost, defaultHttpPort, defaultUnix, "0.0.0.1:5555"); err != nil || addr != "tcp://0.0.0.1:5555" {
t.Errorf("0.0.0.1:5555 -> expected tcp://0.0.0.1:5555, got %s", addr)
}
if addr, err := ParseHost(defaultHttpHost, defaultHttpPort, defaultUnix, ":6666"); err != nil || addr != "tcp://127.0.0.1:6666" {
t.Errorf(":6666 -> expected tcp://127.0.0.1:6666, got %s", addr)
}
if addr, err := ParseHost(defaultHttpHost, defaultHttpPort, defaultUnix, "tcp://:7777"); err != nil || addr != "tcp://127.0.0.1:7777" {
t.Errorf("tcp://:7777 -> expected tcp://127.0.0.1:7777, got %s", addr)
}
if addr, err := ParseHost(defaultHttpHost, defaultHttpPort, defaultUnix, ""); err != nil || addr != "unix:///var/run/docker.sock" {
t.Errorf("empty argument -> expected unix:///var/run/docker.sock, got %s", addr)
}
if addr, err := ParseHost(defaultHttpHost, defaultHttpPort, defaultUnix, "unix:///var/run/docker.sock"); err != nil || addr != "unix:///var/run/docker.sock" {
t.Errorf("unix:///var/run/docker.sock -> expected unix:///var/run/docker.sock, got %s", addr)
}
if addr, err := ParseHost(defaultHttpHost, defaultHttpPort, defaultUnix, "unix://"); err != nil || addr != "unix:///var/run/docker.sock" {
t.Errorf("unix:///var/run/docker.sock -> expected unix:///var/run/docker.sock, got %s", addr)
}
if addr, err := ParseHost(defaultHttpHost, defaultHttpPort, defaultUnix, "udp://127.0.0.1"); err == nil {
t.Errorf("udp protocol address expected error return, but err == nil. Got %s", addr)
}
if addr, err := ParseHost(defaultHttpHost, defaultHttpPort, defaultUnix, "udp://127.0.0.1:4243"); err == nil {
t.Errorf("udp protocol address expected error return, but err == nil. Got %s", addr)
}
}
func TestParseRepositoryTag(t *testing.T) {
if repo, tag := ParseRepositoryTag("root"); repo != "root" || tag != "" {
t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "root", "", repo, tag)
}
if repo, tag := ParseRepositoryTag("root:tag"); repo != "root" || tag != "tag" {
t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "root", "tag", repo, tag)
}
if repo, tag := ParseRepositoryTag("user/repo"); repo != "user/repo" || tag != "" {
t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "user/repo", "", repo, tag)
}
if repo, tag := ParseRepositoryTag("user/repo:tag"); repo != "user/repo" || tag != "tag" {
t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "user/repo", "tag", repo, tag)
}
if repo, tag := ParseRepositoryTag("url:5000/repo"); repo != "url:5000/repo" || tag != "" {
t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "url:5000/repo", "", repo, tag)
}
if repo, tag := ParseRepositoryTag("url:5000/repo:tag"); repo != "url:5000/repo" || tag != "tag" {
t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "url:5000/repo", "tag", repo, tag)
}
}
func TestGetResolvConf(t *testing.T) {
resolvConfUtils, err := GetResolvConf()
if err != nil {
t.Fatal(err)
}
resolvConfSystem, err := ioutil.ReadFile("/etc/resolv.conf")
if err != nil {
t.Fatal(err)
}
if string(resolvConfUtils) != string(resolvConfSystem) {
t.Fatalf("/etc/resolv.conf and GetResolvConf have different content.")
}
}
func TestCheckLocalDns(t *testing.T) {
for resolv, result := range map[string]bool{`# Dynamic
nameserver 10.0.2.3
search dotcloud.net`: false,
`# Dynamic
#nameserver 127.0.0.1
nameserver 10.0.2.3
search dotcloud.net`: false,
`# Dynamic
nameserver 10.0.2.3 #not used 127.0.1.1
search dotcloud.net`: false,
`# Dynamic
#nameserver 10.0.2.3
#search dotcloud.net`: true,
`# Dynamic
nameserver 127.0.0.1
search dotcloud.net`: true,
`# Dynamic
nameserver 127.0.1.1
search dotcloud.net`: true,
`# Dynamic
`: true,
``: true,
} {
if CheckLocalDns([]byte(resolv)) != result {
t.Fatalf("Wrong local dns detection: {%s} should be %v", resolv, result)
}
}
}
func assertParseRelease(t *testing.T, release string, b *KernelVersionInfo, result int) {
var (
a *KernelVersionInfo
)
a, _ = ParseRelease(release)
if r := CompareKernelVersion(a, b); r != result {
t.Fatalf("Unexpected kernel version comparison result. Found %d, expected %d", r, result)
}
}
func TestParseRelease(t *testing.T) {
assertParseRelease(t, "3.8.0", &KernelVersionInfo{Kernel: 3, Major: 8, Minor: 0}, 0)
assertParseRelease(t, "3.4.54.longterm-1", &KernelVersionInfo{Kernel: 3, Major: 4, Minor: 54}, 0)
assertParseRelease(t, "3.4.54.longterm-1", &KernelVersionInfo{Kernel: 3, Major: 4, Minor: 54, Flavor: "1"}, 0)
assertParseRelease(t, "3.8.0-19-generic", &KernelVersionInfo{Kernel: 3, Major: 8, Minor: 0, Flavor: "19-generic"}, 0)
}
func TestDependencyGraphCircular(t *testing.T) {
g1 := NewDependencyGraph()
a := g1.NewNode("a")
b := g1.NewNode("b")
g1.AddDependency(a, b)
g1.AddDependency(b, a)
res, err := g1.GenerateTraversalMap()
if res != nil {
t.Fatalf("Expected nil result")
}
if err == nil {
t.Fatalf("Expected error (circular graph can not be resolved)")
}
}
func TestDependencyGraph(t *testing.T) {
g1 := NewDependencyGraph()
a := g1.NewNode("a")
b := g1.NewNode("b")
c := g1.NewNode("c")
d := g1.NewNode("d")
g1.AddDependency(b, a)
g1.AddDependency(c, a)
g1.AddDependency(d, c)
g1.AddDependency(d, b)
res, err := g1.GenerateTraversalMap()
if err != nil {
t.Fatalf("%s", err)
}
if res == nil {
t.Fatalf("Unexpected nil result")
}
if len(res) != 3 {
t.Fatalf("Expected map of length 3, found %d instead", len(res))
}
if len(res[0]) != 1 || res[0][0] != "a" {
t.Fatalf("Expected [a], found %v instead", res[0])
}
if len(res[1]) != 2 {
t.Fatalf("Expected 2 nodes for step 2, found %d", len(res[1]))
}
if (res[1][0] != "b" && res[1][1] != "b") || (res[1][0] != "c" && res[1][1] != "c") {
t.Fatalf("Expected [b, c], found %v instead", res[1])
}
if len(res[2]) != 1 || res[2][0] != "d" {
t.Fatalf("Expected [d], found %v instead", res[2])
}
}
func TestParsePortMapping(t *testing.T) {
data, err := PartParser("ip:public:private", "192.168.1.1:80:8080")
if err != nil {
t.Fatal(err)
}
if len(data) != 3 {
t.FailNow()
}
if data["ip"] != "192.168.1.1" {
t.Fail()
}
if data["public"] != "80" {
t.Fail()
}
if data["private"] != "8080" {
t.Fail()
}
}
func TestGetNameserversAsCIDR(t *testing.T) {
for resolv, result := range map[string][]string{`
nameserver 1.2.3.4
nameserver 40.3.200.10
search example.com`: {"1.2.3.4/32", "40.3.200.10/32"},
`search example.com`: {},
`nameserver 1.2.3.4
search example.com
nameserver 4.30.20.100`: {"1.2.3.4/32", "4.30.20.100/32"},
``: {},
` nameserver 1.2.3.4 `: {"1.2.3.4/32"},
`search example.com
nameserver 1.2.3.4
#nameserver 4.3.2.1`: {"1.2.3.4/32"},
`search example.com
nameserver 1.2.3.4 # not 4.3.2.1`: {"1.2.3.4/32"},
} {
test := GetNameserversAsCIDR([]byte(resolv))
if !StrSlicesEqual(test, result) {
t.Fatalf("Wrong nameserver string {%s} should be %v. Input: %s", test, result, resolv)
}
}
}
func StrSlicesEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i, v := range a {
if v != b[i] {
return false
}
}
return true
}

View File

@@ -0,0 +1,17 @@
// Copyright 2014 Docker authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the DOCKER-LICENSE file.
package utils
import (
"errors"
)
type Utsname struct {
Release [65]byte
}
func uname() (*Utsname, error) {
return nil, errors.New("Kernel version detection is not available on windows")
}